52 comments

[ 4.1 ms ] story [ 60.6 ms ] thread
As much as I love Java, everybody should just be using Rust. That way you are actually in control, know what's going on, etc. Another reason specifically against Java is that the tooling, both Maven and Gradle, still stucks.
[flagged]
JavaScript can be fast too, it's just the ecosystem and decisions devs make that slow it down.

Same for Java, I have yet to in my entire career see enterprise Java be performant and not memory intensive.

At the end of the day, if you care about performance at the app layer, you will use a language better suited to that.

First request latency also can really suck in Java before hotpathed code gets through the C2 compiler. You can warm up hotpaths by running that code during startup, but it's really annoying having to do that. Using C++, Go, or Rust gets you around that problem without having to jump through the hoops of code path warmup.

I wish Java had a proper compiler.

Do good, don't do bad. Okay.
[flagged]
Understanding algorithmic complexity (in particular, avoiding rework in loops), is useful in any language, and is sage advice.

In practice though, for most enterprise web services, a lot of real world performance comes down to how efficiently you are calling external services (including the database). Just converting a loop of queries into bulk ones can help loads (and then tweaking the query to make good use of indexes, doing upserts, removing unneeded data, etc.)

I'm hopeful that improvements in LLMs mean we can ditch ORMs (under the guise that they are quicker to write queries and the inbetween mapping code with) and instead make good use of SQL to harness the powers that modern databases provide.

I’d say both local data structures and algorithms atop them, and external services like DBs, etc., are both just “resources” in a more abstract sense. Optimizing performance is a matter of using the right resources for the right things. Algorithms help a lot when you’re building FE components (even if the server is rendering them, or “rendering” responses for the FE).

I’d also argue “micro-ORMs” like Diesel (which isn’t really much like ActiveRecord, Hibernate, etc., but more a very thin DSL/interface that maps SQL types to Rust types), combined with LLMs, are the ideal solution (assuming we still want humans to be able to easily understand and trust the code generated). And there’s a big argument to be made for schema migration management being done at the app level (with plain SQL for migrations).

All that said, at work, we use Rails. And ActiveRecord’s “includes/preload/eager_load” methods are fantastic solutions to 99% of cases of querying for things efficiently, and are far more clear than all the SQL you’d have to write to replicate them.

You can write many of the bad examples in the article in any language. It is just far more common to see them in Java code than some other languages.

Java is only fast-ish even on its best day. The more typical performance is much worse because the culture around the language usually doesn't consider performance or efficiency to be a priority. Historically it was even a bit hostile to it.

No - this is not entirely true. Many of Java's fundamental design decisions lead to unexpected slowness that just does not happen in other languages.

For example, appending to a string in a loop. That only happens because of how Java handles strings. In C++, that's very fast. As fast as it can get, really. Since it all goes into the same buffer that gets mutated and expands at a good growth rate. Basically, equivalent to StringBuilder, but that's just all strings.

Or, boxing. C++ doesn't have to box generics to store them in a container.

Also finding the right garbage collector and settings that works best for your project can help a lot.
> Exceptions for Control Flow

This one is so prevalent that JVM has an optimization where it gives up on filling stack for exception, if it was thrown over and over in exact same place.

A subject close to my heart, I write a lot of heavily optimised code including a lot of hot data pipelines in Java.

And aside from algorithms, it usually comes down to avoiding memory allocations.

I have my go-to zero-alloc grpc and parquet and json and time libs etc and they make everything fast.

It’s mostly how idiomatic Java uses objects for everything that makes it slow overall.

But eventually after making a JVM app that keeps data in something like data frames etc and feels a long way from J2EE beans you can finally bump up against the limits that only c/c++/rust/etc can get you past.

Knock Knock

Who’s there?

long pause

Java

this is great, so practical!!!

any other resources like this?

For fillInStackTrace, another trick is to define your own Exception subclass and override the method to be empty. I learned this trick 15+ years ago.

It doesn't excuse the "use exceptions for control flow" anti-pattern, but it is a quick patch.

I ran into 5 and 7 in a Flink app recently - was parsing a timestamp as a number first and then falling back to iso8601 string, which is what it was. The flamegraph showed 10% for the exception handling bit. While fixing that, also found repeated creation of datetimeformatter. Both were not in loops, but both were being done for every event, for 10s of 1000s of events every second.
When you're using a programming language that naturally steers you to write slow code you can't only blame the programmer.

I was listening to someone say they write fast code in Java by avoiding allocations with a PoolAllocator that would "cache" small objects with poolAllocator.alloc(), poolAllocator.release(). So just manual memory management with extra steps. At that point why not use a better language for the task?

> At that point why not use a better language for the task?

Such as?

The autoboxing in a loop case can be handled by the compiler.
The Autoboxing example imo is a case of "Java isn't so fast". Why can't this be optimized behind the scenes by the compiler ?

Rest of advice is great: things compilers can't really catch but a good code reviewer should point out.

Nitpick just because.

Orders by hour could be made faster. The issue with it is it's using a map when an array works both faster and just fine.

On top of that, the map boxes the "hour" which is undesirable.

This is how I'd write it

    long[] ordersByHour = new long[24];
    var deafultTimezone = ZoneId.systemDefault();
    for (Order order : orders) {
        int hour = order.timestamp().atZone(deafultTimezone).getHour();
        ordersByHour[hour]++;
    }
If you know the bound of an array, it's not large, and you are directly indexing in it, you really can't do any better performance wise.

It's also not less readable, just less familiar as Java devs don't tend to use arrays that much.

Any non-trivial program that has never had an optimizer run on it has a minimal-effort 50+% speedup in it.
The code:

  public int parseOrDefault(String value, int defaultValue) {
      if (value == null || value.isBlank()) return defaultValue;
      for (int i = 0; i < value.length(); i++) {
          char c = value.charAt(i);
          if (i == 0 && c == '-') continue;
          if (!Character.isDigit(c)) return defaultValue;
      }
      return Integer.parseInt(value);
  }
Is probably worse than Integer.parseInt alone, since it can still throw NumberFormatExceptions for values that overflow (which is no longer handled!). Would maybe fix that. Unfortunately this is a major flaw in the Java standard library; parsing numbers shouldn't throw expensive exceptions.
Avoiding Java's string footguns is an interesting problem in programming languages design.

The String.format() problem is most immediately a bad compiler and bad implementation, IMO. It's not difficult to special-case literal strings as the first argument, do parsing at compile time, and pass in a structured representation. The method could also do runtime caching. Even a very small LRU cache would fix a lot of common cases. At the very least they should let you make a formatter from a specific format string and reuse it, like you can with regexes, to explicitly opt into better performance.

But ultimately the string templates proposal should come back and fix this at the language level. Better syntax and guaranteed compile-time construction of the template. The language should help the developer do the fast thing.

String concatenation is a little trickier. In a JIT'ed language you have a lot of options for making a hierarchy of string implementations that optimize different usage patterns, and still be fast - and what you really want for concatenation is a RopeString, like JS VMs have, that simply references the other strings. The issue is that you don't want virtual calls for hot-path string method calls.

Java chose a single final class so all calls are direct. But they should have been able to have a very small sealed class hierarchy where most methods are final and directly callable, and the virtual methods for accessing storage are devirtualized in optimized methods that only ever see one or two classes through a call site.

To me, that's a small complexity cost to make common string patterns fast, instead of requiring StringBuilder.

Perhaps something like zig’s comptime would help a bit.
It's interesting to see how something like Common Lisp handles the format issue.

In CL, there's a general infrastructure called "compiler macros" that is intended as a hint to the compiler to expand calls as macros at compile time. The macro is also allowed to just leave the form unexpanded, in which case it defaults to an unexpanded function call. And the function can be turned into a value itself and passed around, even if the compiler macro exists.

For CL's format, this means an implementation will typically have a compiler macro (or some similar mechanism) that does an expansion if the format is a string constant.

CL also has a function called formatter that takes a format string and returns a function that acts like (lambda (&rest args) (apply #'format <the format string> args). This function can be implemented as something that expands the format string into code and then compiles the code.

The mechanisms in CL would allow a user to implement the equivalent of a format compiler macro (and formatter) even if the implementation didn't provide them.

I thought those were common sense until I worked on a program written by my colleague recently.