Live data from Hacker News

Tune Code Before Your Garbage Collector

blog.vanillajava.blog

11–20 of 41 posts

Re: Tune Code Before Your Garbage Collector

#11
post #10

Reading this gives me considerable pause - I can’t think of many classes within the codebase I work on that don’t have @Slf4j at the top… Since there wasn’t a link to the source code in that post, can you help me understand this - for the SLF4J baseline is your logger impl a console appender, a file appender, or a network service like an OTel collector? Does any of that matter for GC context?

All common logging backends create a LogEvent or similar object for each logging call, and logging calls also typically construct new strings, which usually means a new StringBuilder object, its internal array (multiple ones if it grows), the final array it is copied to, and the String object that wraps that array. These are typically short-lived objects and therefore cheap. Nevertheless, continually creating many su…

> All common logging backends create a LogEvent or similar object for each logging call, and logging calls also typically construct new strings, which usually means a new StringBuilder object, its internal array (multiple ones if it grows), the final array it is copied to, and the String object that wraps that array.

Which then gets discarded because that was a Log.verbose and your minimum log level in production is WARN.

Which is why many libraries have moved towards making your log message returned by a lambda. One constant lambda allocation (so, not a lot, an invokedynamic is absolutely fuck all.) that allows you to straight up skip allocating a full string that most likely is interpolating things and attempting to reach for context present on other threads is strictly better in 99.9% of the cases. The GC pressure is kept minimal and most importantly, constant.

Re: Tune Code Before Your Garbage Collector

#12
post #10

Reading this gives me considerable pause - I can’t think of many classes within the codebase I work on that don’t have @Slf4j at the top… Since there wasn’t a link to the source code in that post, can you help me understand this - for the SLF4J baseline is your logger impl a console appender, a file appender, or a network service like an OTel collector? Does any of that matter for GC context?

All common logging backends create a LogEvent or similar object for each logging call, and logging calls also typically construct new strings, which usually means a new StringBuilder object, its internal array (multiple ones if it grows), the final array it is copied to, and the String object that wraps that array. These are typically short-lived objects and therefore cheap. Nevertheless, continually creating many su…

Cheap, not free, and even pretty simple to accidentally fool the GC on the lifetime of these objects.

Consider, for example, if you have a log message like this

    logger.info("Hello {}", myOldObject);
if "myOldObject" is large enough or contains references to large things or has just been around for a while, it may be a part of OldGen at this point. And if that's the case, the LogEvent objects will end up automatically promoted to OldGen. Meaning the only time those can be be claimed is in an expensive major collection. The end result is that these things will ultimately fill up old gen and trigger more of the expensive old gen collections.

That's why it can be faster in some circumstances to write the more wordy

    if (logger.isInfoEnabled()) {
      logger.info("Hello {}", myOldObject.toString());
    }
Nothing saves you, however, if your string being logged is too long. It can be autopromoted to old gen if you are trying to log a 10mb string.

Re: Tune Code Before Your Garbage Collector

#13
post #9
post #8

Earlier quoted context omitted.

I believe the whole string vs stringbuffer that later was made redundant by compiler contributed to that vision. People started dismissing allocation discipline as a thing from the past because "that thing was solved a lot ago and the compiler now is smart enough". Well, for string, yes, but not for arbitrary objects.

The most surprising allocation pressure I constantly run into is primitive boxing. The JVM does heroics to try and avoid it as much as possible, but when you end up with some primitive boxing in a hotspot the amount of GC pressure that creates can be unreal.

Yep, and sometimes just a small code change can flip on boxing.

Re: Tune Code Before Your Garbage Collector

#14

Garbage collector? To quote Bjarne Stroustrup: > I don't like garbage. I don't like littering. My ideal is to eliminate the need for a garbage collector by not producing any garbage. That is now possible.

[flagged]

I prefer to see C++ as a failed experiment that just keeps going and going rather than garbage. The software industry learned a lot from it, both good and bad. But yea, I haven’t programmed in it since the late 1990s.

Re: Tune Code Before Your Garbage Collector

#15
post #13
post #9

Earlier quoted context omitted.

The most surprising allocation pressure I constantly run into is primitive boxing. The JVM does heroics to try and avoid it as much as possible, but when you end up with some primitive boxing in a hotspot the amount of GC pressure that creates can be unreal.

Yep, and sometimes just a small code change can flip on boxing.

One of my least proud (most proud?) hacks when working with very large data sets is something like this

    Map intCache = new HashMap();
    
    while (loading) {
      Integer feild1 = intCache.computeIfAbsent(getField1(), (i)->i);
    }
This is a terrible thing that shouldn't be as useful as it is to us... but it is really useful. We have a bunch of objects that can optionally have Integer values (hence a null is valid) but those int values are frequently the same.

This saves a bunch of memory and ultimately GC pressure as a result.

Valhalla can't come soon enough for us.

Re: Tune Code Before Your Garbage Collector

#16
post #10

Earlier quoted context omitted.

All common logging backends create a LogEvent or similar object for each logging call, and logging calls also typically construct new strings, which usually means a new StringBuilder object, its internal array (multiple ones if it grows), the final array it is copied to, and the String object that wraps that array. These are typically short-lived objects and therefore cheap. Nevertheless, continually creating many su…

> All common logging backends create a LogEvent or similar object for each logging call, and logging calls also typically construct new strings, which usually means a new StringBuilder object, its internal array (multiple ones if it grows), the final array it is copied to, and the String object that wraps that array. Which then gets discarded because that was a Log.verbose and your minimum log level in production is…

> Which then gets discarded because that was a Log.verbose and your minimum log level in production is WARN.

This isn't true for the LogEvent or equivalent object, which only gets created after the log level is tested to be applicable by the logger implementation.

For call-site object allocation, you can wrap the logging call into an if statement that checks for the corresponding log level. The lambda allocation isn't constant if it captures anything from the surrounding scope, which will generally be the case for logging calls. (Unless by "constant" you mean that it's a single allocation per execution.)

Re: Tune Code Before Your Garbage Collector

#17
post #13

Earlier quoted context omitted.

Yep, and sometimes just a small code change can flip on boxing.

One of my least proud (most proud?) hacks when working with very large data sets is something like this Map intCache = new HashMap (); while (loading) { Integer feild1 = intCache.computeIfAbsent(getField1(), (i)->i); } This is a terrible thing that shouldn't be as useful as it is to us... but it is really useful. We have a bunch of objects that can optionally have Integer values (hence a null is valid) but those int…

I do something similar for Java Time local dates. Financial data in particular has lots of redundant date info and benefits from being memoized. Converting to epoch millis also works.

Re: Tune Code Before Your Garbage Collector

#18
post #9
post #8

Earlier quoted context omitted.

I believe the whole string vs stringbuffer that later was made redundant by compiler contributed to that vision. People started dismissing allocation discipline as a thing from the past because "that thing was solved a lot ago and the compiler now is smart enough". Well, for string, yes, but not for arbitrary objects.

The most surprising allocation pressure I constantly run into is primitive boxing. The JVM does heroics to try and avoid it as much as possible, but when you end up with some primitive boxing in a hotspot the amount of GC pressure that creates can be unreal.

can't wait for Project Valhalla, going into preview shortly

Re: Tune Code Before Your Garbage Collector

#19
post #17

Earlier quoted context omitted.

One of my least proud (most proud?) hacks when working with very large data sets is something like this Map intCache = new HashMap (); while (loading) { Integer feild1 = intCache.computeIfAbsent(getField1(), (i)->i); } This is a terrible thing that shouldn't be as useful as it is to us... but it is really useful. We have a bunch of objects that can optionally have Integer values (hence a null is valid) but those int…

I do something similar for Java Time local dates. Financial data in particular has lots of redundant date info and benefits from being memoized. Converting to epoch millis also works.

If I squint, is this a special kind of heap compression?

Re: Tune Code Before Your Garbage Collector

#20
post #14

Earlier quoted context omitted.

[flagged]

I prefer to see C++ as a failed experiment that just keeps going and going rather than garbage. The software industry learned a lot from it, both good and bad. But yea, I haven’t programmed in it since the late 1990s.

I'd phrase it differently, C++ was a set of power-to-performance trade-offs that were optimal in the 1990s.

Time has moved on.

More importantly, a typical 1990s C++ dev was likely someone who learned assembly, then C or C++. Meaning they already knew how to control hardware / memory allocation, and C++ was just a new set of abstraction tools. It was a step forward for them.

To modern devs, C++ is a step backwards. And a tough one at that.

Post reply on HN