I'm genuinely curious about the effect, but I simply don't have patience for the AI writing. Can anyone give an actual non-garbage explanation with some respect for the reader?
I'm genuinely curious: I read through the article and the narrative felt deliberate and didn't trigger my AI-radar. What made you think it was AI written?
It’s rife with “and that’s the important thing. $thing”-type constructions. Human writing uses those much more sparingly and doesn’t separate them into two sentences as often. It has a lot of “subject change: teaser” constructions, too. And overuse of bold and restatement.
Could it be human? Sure. But it doesn’t seem likely to me.
> This article is the story of chasing that number down to a single machine instruction, and then finding out that the instruction was only half of the answer.
> So the difference has to be in what the JIT generated, and the profiler gives us exactly that.
> That is the whole vocabulary. Let’s read some code.
> Decoding the x86 version instruction by instruction is out of scope here.
Even if that were AI (and I think there are better examples in the article to indicate that it is), why do you care if you just used AI anyway to summarize it anyway?
Some of us have been here longer than AI. "To be intellectually stimulated" would be the why, which there used to be more of prior to the frontpage becoming riddled with AI slop. "HN is for conversation between humans" as the guidelines say, and though the guidelines imply that's towards comments, I'd prefer it to be towards submissions too, for the same reasons it was added for comments.
I'm already at the point of "the reward/effort of HN is getting pretty low", but the question is, where to leave for?
1. Upgrade your JDK for the best performance (as the article says, the slowdown is gone in JDK 26).
2. Don't try to help the GC by pooling objects. Mutating old objects can be expensive, while allocating new ones is cheap (at least for objects that don't do some exceptionally expensive initialisation).
Object pooling still has its place, but like any optimization it needs to be based on benchmarks and shouldn't be done haphazardly. Blindly pooling objects will lead to regressions and resource contention more often than improvements.
There are also middle ground options, like pooling objects but giving the pool a lifecycle that is tied to a request.
The problem is that 1. it's not easy to beat the JDK's GCs at memory management (assuming you've picked the right GC for your workload) especially as they keep getting better and better, and 2. how a pool behaves relative to the GC depends greatly on the GC algorithm (e.g. the same pool could help a bit with, say, Parallel GC, and hurt significantly with G1 or ZGC), and the different GC algorithms also tend to change significantly from release to release, so it's hard to write a good pool that can remain good both across different GCs and across different runtime versions.
In particular, the JDK's GCs are heavily optimised for short-lived objects with high allocation rates. What you want to avoid is temporary data finding itself in the old gen. The newer GCs may dynamically size the young generation to match your program's natural meaning of "short lived", but the longer an object lives, the higher the chances it ends up in the old gen. You want only objects that stick around for a very long time (and have a low allocation rate) to end up in old gen. If you just write naive code, chances are things will work out well. Once you start being clever, you're taking a risk.
So if you're willing to profile your program, with a workload that's representative of production workload (a microbenchmark is useless) on every runtime version and potentially change your "manual optimisation" every six months, you can try. But if not, the advice for the best performance over time is to rely on the platform and let it do its thing. The reason is that the JVM is continuously being optimised for "normal programs". If you're doing anything too clever, you may find that in a future release, your code is making things worse because the optimisations that target normal code don't help your code (or could even treat it as unusual and have it hit slow paths).
I once spoke to a company that were very proud in getting something like a 10% improvement over naive code in Java 8, thanks to some hand optimisation they worked a lot on, only to discover that it caused a 15% regression compared to doing nothing special on JDK 11.
Where pooling does sometimes win is for stuff like intermediate buffers for compression or decompression, since a Java alloc will zero the memory, which for sufficiently large buffers is much costlier than the allocation itself, and in such a case you don't care if it's zeroed.
Removing allocation pressure can also have effects on other parts of the system, but that is anything but trivial to measure or reason about.
I am happy to see JDK versions actually becoming faster and lighter over time. Nice contrast with other platforms that seem to be moving in the opposite direction.
Honestly, I don't really understand why G1 is being pushed so hard.
The parallel collector is a perfectly fine collector, particularly for smaller heaps. Even the serial collector isn't bad for things like a containerized environment, yet G1 replaces it by default now [1].
It's not a bad algorithm, but especially when you start talking about sub 2G environments I've not seen a situation where the parallel and serial collectors won't handily beat G1 on pretty much every metric. Major collectors with modern CPUs just doesn't take much time for a lot of memory.
If anything, I think it's not unlikely that ZGC will become the default at some point, as it matures. It's hard to beat Parallel on batch workloads, although G1 is getting there. ZGC is unparalleled for low-latency (GC pauses are just gone). G1 is intended to offer a compromise that could be a reasonable default.
I have no qualms with ZGC being the default. The low latency that it offers at near G1 speeds is a very good trade off (IMO).
I just have a problem with G1 because in my experience, the best place for it is fairly large heaps. Get something sub 2 or even 10G, especially if you have a few cores to offer, and the parallel and often even the serial collector will give G1 latency even on major collections with superior throughput and overhead.
I would be very surprised if ZGC became the default, because it incurs a significant overhead penalty to eliminate those GC pauses. All else equal you're effectively just sacrificing throughput for latency, since it's doing a bunch of extra housekeeping in the background (foreshadowing...) That's a perfectly reasonable tradeoff to make if low latency is a priority (or perhaps more importantly if having very consisent/predictable latency is a priority) but in most Java projects I've been exposed to that's been a tertiary concern at best. Frankly, I question if most Java developers are even aware that they're allocating physical memory when they type 'new'...
In the modern enterprise Java world (that I've been exposed to) it's very common to have a mandate that all components deploy a minimum of N instances across X regions for resiliency. By design that almost always means you're deploying at least 2x more compute than you strictly need, so the top priority is generally minimizing per-instance overhead to minimize cloud spend.
For example, the default templates at my current company deploy something like 0.25-0.5 vCPU per instance, and therin lies the rub. ZGC performance is _catastrophically_ bad with <=1 cpus because when there's only one core, any "concurrent" GC events become full on stop the world events. We had someone pilot a change to the default JVM args for all components because they heard that ZGC would reduce latency, only to discover that basically all of our microservices immediately failed their perf tests. For the first one I spot checked, throughput was down ~90% and p95 went from ~40ms to >1s, because more time was being spent on "background" GC than actually servicing requests.
Hope that didn't come off adversarial. I just find GC fascinating, and ended up spending a bunch of time working with the team that owns those defaults to draft general recommendations. TLDR is that when in doubt don't specify/let the JVM pick for you, and don't be surprised if it picks serial :)
All Java GCs are generational collectors, they reduce the marking time (for young collection) by tracking if there is a reference from the old generation to the new generation.
The benchmark creates an array in the old generation (by being big enough) and stores an object (allocated in the new generation). This triggers the GC barrier for every writes. Something rare in real application.
The G1 barrier before Java 26 is slow because:
- the GC barrier and some GC threads do concurrent operations on the same memory zone (the card table)
- the barrier is big (a lot of assembler instructions) so it also troubles the loop unrolling optimization performed by JITs
Parallel GC has a simple barrier and do not care about latency (no GC check inside the loop).
The barrier implementation of G1GC was changed in Java 26, so update your Java runtime version and move on.
I read through it. Not bad. In fact, pretty good. It is about something that they actually did.
tl;dr: In JDk 25, filling a large array of references with objects living in a different heap region is extremely slow when using G1 GC as opposed to parallel GC. Solution: Move to Java 26. Or increase G1HeapRegionSize.
Details:
They used Amazon Corretto as JDK. When Arrays.fill() was called with UseG1GC and UseParallelGC, the former was really, really slow.
Then they go on to give an ARM64 primer (because they ran it on an Apple M4 Max).
The part where they mention g1BarrierSetAssembler_x86.cpp and g1BarrierSetAssembler_aarch64.cpp is where at least some readers get lost.
They are essentially saying that the logic (for both x86 and ARM64) is the same. The reason why only those two files matter for this test, I believe, comes down to JEP 304[1]. Just accept it and continue reading. (And validate it later if you want to.)
They list around twelve lines of ARM instructions that matter for this test - cheat sheet, essentially.
The three lines of actually generated ARM instructions are shown then for parallel GC (the much faster one in this case).
They explain what a write barrier is (basically GC bookkeeping). So as a result, there are three instructions, but they are neat and tight. Also, we see that eight elements are filled in each iteration of the loop which fills the arrays.
Then, they show what was generated for G1 GC.
And it is indeed too long. Around 20 instructions. However, some of them shouldn't have been executed because of three exit conditions. They explain why none of them fired.
They prove that by subtracting eight from the humongous threshold size, it is allocated to an Eden/young region and the problem vanishes because one of the three exit conditions is triggered thus.
Then, they show that it is a problem that increases almost linearly (my guess, based on the numbers) with the array size- so the larger the array size is, the more the time delay!!
Finally, the solution: JDK 26 took care of this issue. They also point to JEP 522 (I didn't go through it yet). If you have an older version, use the G1HeapRegionSize flag if you can do so.
That is it, essentially. Definitely long, but well-written, based on actual testing.
I'm genuinely curious about the effect, but I simply don't have patience for the AI writing. Can anyone give an actual non-garbage explanation with some respect for the reader?
Slightly less annoying summary from ChatGPT free: https://chatgpt.com/share/6a9ac7a3-15a0-83eb-8c2a-6f72cd9beb....
Caveat emptor: it makes high level sense, but I haven’t thought about it in detail.
I'm genuinely curious: I read through the article and the narrative felt deliberate and didn't trigger my AI-radar. What made you think it was AI written?
It’s rife with “and that’s the important thing. $thing”-type constructions. Human writing uses those much more sparingly and doesn’t separate them into two sentences as often. It has a lot of “subject change: teaser” constructions, too. And overuse of bold and restatement.
Could it be human? Sure. But it doesn’t seem likely to me.
It didn’t read AI generated to me. I also love the irony of then using AI to generate a summary
> This article is the story of chasing that number down to a single machine instruction, and then finding out that the instruction was only half of the answer.
> So the difference has to be in what the JIT generated, and the profiler gives us exactly that.
> That is the whole vocabulary. Let’s read some code.
> Decoding the x86 version instruction by instruction is out of scope here.
> What is not architecture specific is the logic.
Here’s a segment flagged by Pangram: https://www.pangram.com/history/87e25169-30a4-4030-a65a-dba8...
I don't know if it's AI or not, but if it is, it isn't egregious, and certainly not "garbage" or disrespectful to the reader.
I will admit I shouldn’t have said “garbage”, because it is too emotional.
But I absolutely think it is bad for the reader, and deserves to be called out. Authors should know that it’s not good enough.
I don't think the Pangram result is shareable to others.
"If you are sure the link is correct, please make sure you are logged in with the account associated with this result."
Even if that were AI (and I think there are better examples in the article to indicate that it is), why do you care if you just used AI anyway to summarize it anyway?
I don’t really care that it’s AI, the problem is that it’s bad AI writing.
The summary I shared is much more straightforward. It’s mediocre, but just barely good enough to extract the message without making me super annoyed.
[dead]
If you don't like AI writing, why are you on hacker news? Most of the articles posted are written by AI.
Some of us have been here longer than AI. "To be intellectually stimulated" would be the why, which there used to be more of prior to the frontpage becoming riddled with AI slop. "HN is for conversation between humans" as the guidelines say, and though the guidelines imply that's towards comments, I'd prefer it to be towards submissions too, for the same reasons it was added for comments.
I'm already at the point of "the reward/effort of HN is getting pretty low", but the question is, where to leave for?
There are two practical lessons here:
1. Upgrade your JDK for the best performance (as the article says, the slowdown is gone in JDK 26).
2. Don't try to help the GC by pooling objects. Mutating old objects can be expensive, while allocating new ones is cheap (at least for objects that don't do some exceptionally expensive initialisation).
Object pooling still has its place, but like any optimization it needs to be based on benchmarks and shouldn't be done haphazardly. Blindly pooling objects will lead to regressions and resource contention more often than improvements.
There are also middle ground options, like pooling objects but giving the pool a lifecycle that is tied to a request.
The problem is that 1. it's not easy to beat the JDK's GCs at memory management (assuming you've picked the right GC for your workload) especially as they keep getting better and better, and 2. how a pool behaves relative to the GC depends greatly on the GC algorithm (e.g. the same pool could help a bit with, say, Parallel GC, and hurt significantly with G1 or ZGC), and the different GC algorithms also tend to change significantly from release to release, so it's hard to write a good pool that can remain good both across different GCs and across different runtime versions.
In particular, the JDK's GCs are heavily optimised for short-lived objects with high allocation rates. What you want to avoid is temporary data finding itself in the old gen. The newer GCs may dynamically size the young generation to match your program's natural meaning of "short lived", but the longer an object lives, the higher the chances it ends up in the old gen. You want only objects that stick around for a very long time (and have a low allocation rate) to end up in old gen. If you just write naive code, chances are things will work out well. Once you start being clever, you're taking a risk.
So if you're willing to profile your program, with a workload that's representative of production workload (a microbenchmark is useless) on every runtime version and potentially change your "manual optimisation" every six months, you can try. But if not, the advice for the best performance over time is to rely on the platform and let it do its thing. The reason is that the JVM is continuously being optimised for "normal programs". If you're doing anything too clever, you may find that in a future release, your code is making things worse because the optimisations that target normal code don't help your code (or could even treat it as unusual and have it hit slow paths).
I once spoke to a company that were very proud in getting something like a 10% improvement over naive code in Java 8, thanks to some hand optimisation they worked a lot on, only to discover that it caused a 15% regression compared to doing nothing special on JDK 11.
Where pooling does sometimes win is for stuff like intermediate buffers for compression or decompression, since a Java alloc will zero the memory, which for sufficiently large buffers is much costlier than the allocation itself, and in such a case you don't care if it's zeroed.
Removing allocation pressure can also have effects on other parts of the system, but that is anything but trivial to measure or reason about.
I am happy to see JDK versions actually becoming faster and lighter over time. Nice contrast with other platforms that seem to be moving in the opposite direction.
Honestly, I don't really understand why G1 is being pushed so hard.
The parallel collector is a perfectly fine collector, particularly for smaller heaps. Even the serial collector isn't bad for things like a containerized environment, yet G1 replaces it by default now [1].
It's not a bad algorithm, but especially when you start talking about sub 2G environments I've not seen a situation where the parallel and serial collectors won't handily beat G1 on pretty much every metric. Major collectors with modern CPUs just doesn't take much time for a lot of memory.
[1] https://openjdk.org/jeps/523
If anything, I think it's not unlikely that ZGC will become the default at some point, as it matures. It's hard to beat Parallel on batch workloads, although G1 is getting there. ZGC is unparalleled for low-latency (GC pauses are just gone). G1 is intended to offer a compromise that could be a reasonable default.
I have no qualms with ZGC being the default. The low latency that it offers at near G1 speeds is a very good trade off (IMO).
I just have a problem with G1 because in my experience, the best place for it is fairly large heaps. Get something sub 2 or even 10G, especially if you have a few cores to offer, and the parallel and often even the serial collector will give G1 latency even on major collections with superior throughput and overhead.
I would be very surprised if ZGC became the default, because it incurs a significant overhead penalty to eliminate those GC pauses. All else equal you're effectively just sacrificing throughput for latency, since it's doing a bunch of extra housekeeping in the background (foreshadowing...) That's a perfectly reasonable tradeoff to make if low latency is a priority (or perhaps more importantly if having very consisent/predictable latency is a priority) but in most Java projects I've been exposed to that's been a tertiary concern at best. Frankly, I question if most Java developers are even aware that they're allocating physical memory when they type 'new'...
In the modern enterprise Java world (that I've been exposed to) it's very common to have a mandate that all components deploy a minimum of N instances across X regions for resiliency. By design that almost always means you're deploying at least 2x more compute than you strictly need, so the top priority is generally minimizing per-instance overhead to minimize cloud spend.
For example, the default templates at my current company deploy something like 0.25-0.5 vCPU per instance, and therin lies the rub. ZGC performance is _catastrophically_ bad with <=1 cpus because when there's only one core, any "concurrent" GC events become full on stop the world events. We had someone pilot a change to the default JVM args for all components because they heard that ZGC would reduce latency, only to discover that basically all of our microservices immediately failed their perf tests. For the first one I spot checked, throughput was down ~90% and p95 went from ~40ms to >1s, because more time was being spent on "background" GC than actually servicing requests.
Hope that didn't come off adversarial. I just find GC fascinating, and ended up spending a bunch of time working with the team that owns those defaults to draft general recommendations. TLDR is that when in doubt don't specify/let the JVM pick for you, and don't be surprised if it picks serial :)
2: Dont optimize. Dont optimize, yet. If you must optimize, use a profiler.
All Java GCs are generational collectors, they reduce the marking time (for young collection) by tracking if there is a reference from the old generation to the new generation.
The benchmark creates an array in the old generation (by being big enough) and stores an object (allocated in the new generation). This triggers the GC barrier for every writes. Something rare in real application.
The G1 barrier before Java 26 is slow because:
- the GC barrier and some GC threads do concurrent operations on the same memory zone (the card table)
- the barrier is big (a lot of assembler instructions) so it also troubles the loop unrolling optimization performed by JITs
Parallel GC has a simple barrier and do not care about latency (no GC check inside the loop).
The barrier implementation of G1GC was changed in Java 26, so update your Java runtime version and move on.
I read through it. Not bad. In fact, pretty good. It is about something that they actually did.
tl;dr: In JDk 25, filling a large array of references with objects living in a different heap region is extremely slow when using G1 GC as opposed to parallel GC. Solution: Move to Java 26. Or increase G1HeapRegionSize.
Details:
They used Amazon Corretto as JDK. When Arrays.fill() was called with UseG1GC and UseParallelGC, the former was really, really slow.
Then they go on to give an ARM64 primer (because they ran it on an Apple M4 Max).
The part where they mention g1BarrierSetAssembler_x86.cpp and g1BarrierSetAssembler_aarch64.cpp is where at least some readers get lost. They are essentially saying that the logic (for both x86 and ARM64) is the same. The reason why only those two files matter for this test, I believe, comes down to JEP 304[1]. Just accept it and continue reading. (And validate it later if you want to.)
They list around twelve lines of ARM instructions that matter for this test - cheat sheet, essentially.
The three lines of actually generated ARM instructions are shown then for parallel GC (the much faster one in this case).
They explain what a write barrier is (basically GC bookkeeping). So as a result, there are three instructions, but they are neat and tight. Also, we see that eight elements are filled in each iteration of the loop which fills the arrays.
Then, they show what was generated for G1 GC. And it is indeed too long. Around 20 instructions. However, some of them shouldn't have been executed because of three exit conditions. They explain why none of them fired.
They prove that by subtracting eight from the humongous threshold size, it is allocated to an Eden/young region and the problem vanishes because one of the three exit conditions is triggered thus.
Then, they show that it is a problem that increases almost linearly (my guess, based on the numbers) with the array size- so the larger the array size is, the more the time delay!!
Finally, the solution: JDK 26 took care of this issue. They also point to JEP 522 (I didn't go through it yet). If you have an older version, use the G1HeapRegionSize flag if you can do so.
That is it, essentially. Definitely long, but well-written, based on actual testing.
[1] https://openjdk.org/jeps/304
[2] https://openjdk.org/jeps/522
Thank you - very useful summary.