DEV Community

Zmey56
Zmey56

Posted on

No, Go 1.26 Did Not Make Your Service 40% Faster

Since spring, Green Tea has been coming up in almost every interview I sit in on. And nine times out of ten the answer I hear is some version of "well, it's 40% faster." Ask a follow-up — faster at what? — and the room goes quiet.

The percentages are real. They just don't measure what most people assume they measure. Let's take this apart.

Start with the janitor

Picture a room full of toys. Some are still in use, some aren't. Somebody has to walk around and throw out what's no longer needed, or the room fills up.

In Go, that somebody is the garbage collector. Your program allocates data, stops using it, and the collector comes along and reclaims the space.

Go 1.26 shipped on February 10, 2026, and the janitor works differently now. The approach is called Green Tea, it's on by default, and you don't change any code to get it.

How it used to work

Objects in memory point at each other. One object knows about another, that one knows about a third. You get threads running from toy to toy.

The old collector followed those threads. Take an object, mark it as live, look at what it references, move on. Object by object, until everything reachable has been walked.

The logic is fine. That's not where the problem is.

The problem is that objects are scattered across memory. The first one is at the start, the second is far away, the third is somewhere else again. A janitor following threads ends up sprinting around the whole house: bedroom to kitchen, kitchen to attic, then back to the bedroom.

Why the sprinting is expensive

A CPU has a small, fast memory close at hand — the cache. Think of it as your pocket: whatever's in there, you grab instantly. Then there's the big, slow memory, which is more like a storage room at the far end of the house.

When the CPU needs data, it checks the pocket first. Not there? Walk to the storage room. That walk can cost up to a hundred times more than the pocket.

Now put the two facts together: objects are scattered, and every hop along a reference thread is a jump to a random address. Nearly every step the old janitor took was a trip to the storage room.

The Go team broke the cost down:

  • roughly 90% of collector time goes to marking live objects; only about 10% goes to actually freeing memory;
  • of that marking time, at least 35% is the CPU simply stalled, waiting on memory;
  • Go programs not uncommonly spend 20% or more of their CPU time in the garbage collector.

The Go blog frames it as a driving problem: the CPU wants a highway, and the old algorithm keeps it on city streets. No visibility around corners, traffic lights, pedestrians. It doesn't matter what engine you've got if there's nowhere to open it up.

The idea: pages instead of objects

It fits in one line:

Work on pages, not on individual objects.

One clarification about the word "page," because this is where everyone trips up — myself included, the first time I read the design doc. In Go, a page is an 8 KiB block, and it's Go's own internal unit. It has nothing to do with your operating system's virtual memory page size; the Go blog goes out of its way to say so. Each page holds objects of a single size class.

The mechanics from there: the work list holds pages rather than objects, which makes it dramatically shorter. Every object gets a second mark bit — there used to be one ("seen"), now there are two ("seen" and "scanned"). When the collector finds a reference to an object, it pushes the entire page onto the work list and sets that object's "seen" bit.

The real trick is not being in a hurry

This is the part most write-ups drop, and it's the interesting bit.

The old collector behaved like a stack: push on top, pop from the top immediately. Green Tea behaves like a queue: push to the back, and you won't get to it for a while.

Why do that on purpose? Because while a page sits in the queue, more objects on it get discovered and marked "seen." By the time the page comes up, the collector doesn't scan one object — it scans everything that piled up, in the order those objects sit in memory. The same page can cycle through the work list several times per GC cycle, and that's by design.

The sluggishness isn't a side effect here. It is the mechanism.

Why "in order" is the whole game

CPUs have a prefetcher. It watches your access pattern, and when it sees you reading sequentially, it pulls the next chunk in before you ask for it, while you're still busy with the current one. The data arrives before the request does, so nobody waits.

But it can only predict sequential access. Random jumps give it nothing to work with.

The old collector got zero benefit from it. Green Tea gets all of it. On top of that, the page's own mark metadata now tends to stay in cache, and a shorter work list means less contention between cores.

Vectors: reading in batches

Once your access pattern is predictable, another door opens.

Modern CPUs have vector instructions that process a batch of values per instruction instead of one. Go 1.26 added this acceleration for x86 starting at Intel Ice Lake and AMD Zen 4. Those chips have 512-bit registers, which is wide enough to hold an entire page's mark metadata in a couple of registers, right there in the CPU. They also have a dedicated bit-manipulation instruction that collapses the key scanning step into a handful of cycles.

None of this was possible with the old algorithm. Objects of wildly different sizes, work units ranging from two bits to ten thousand — no regularity, nothing to vectorize.

The vector path is worth roughly another 10% on top of the base improvement. On older CPUs you keep the sequential-access win, which is smaller but still real.

Now, honestly, about that 10–40%

Back to where we started.

The 10–40% is a reduction in time spent by the garbage collector. Not in your program's runtime.

Straight from the Go blog: if your application spends 10% of its CPU time in the collector, the saving works out to somewhere between 1% and 4% of total CPU. The most common outcome is around 10% of GC time — the bottom of the range, not the top.

That's still free performance for a version bump, and I'll take it. But "Go 1.26 made my service 40% faster" is not a sentence about Green Tea.

What to change in your code

Nothing. It's on by default. Upgrade Go, rebuild, done.

If you need the old behavior back, the opt-out happens at build time:

GOEXPERIMENT=nogreenteagc go build ./...
Enter fullscreen mode Exit fullscreen mode

Note that it's a build flag, not a runtime one. Sticking it in front of an already-compiled binary does nothing at all.

It's also temporary. The Go 1.26 release notes state plainly that the opt-out is expected to disappear in Go 1.27, so don't build anything long-term around it. And if you did have to disable it because of a performance regression, the Go team asks you to file an issue — those are exactly the cases they want.

When Green Tea doesn't help

Nobody writes this part up.

Green Tea bets that a page will accumulate enough objects to pay for the accumulation itself. When your heap is regular — same-size objects at similar depths — that bet pays off nicely.

But some workloads end up with exactly one object per page, over and over. Then you're paying the batching cost and getting nothing back, and the result can be worse than the old algorithm. The implementation special-cases those pages, which softens the hit without eliminating it.

The threshold turned out to be surprisingly low, though: per the Go team, the approach starts winning even when only about 2% of a page gets scanned per visit.

Separately: short-lived CLI tools get close to nothing out of this, since the collector barely runs there. And without vector support you don't get that extra 10%.

There's one more limitation Green Tea doesn't address and never set out to. Go's collector doesn't move objects. If a single object on a page is still alive, the whole page stays resident. A program with a badly fragmented heap runs into exactly that, and no amount of scanning speed will save it.

How to check on your own project

You don't have to trust numbers from articles, including this one. Measuring is easy enough.

# Green Tea (default in 1.26)
go build ./... && GODEBUG=gctrace=1 ./your-app

# same thing with the old collector
GOEXPERIMENT=nogreenteagc go build ./... && GODEBUG=gctrace=1 ./your-app

go test -bench=. -benchmem
Enter fullscreen mode Exit fullscreen mode

Watch three things under your actual production load, not a synthetic benchmark: the share of CPU going to GC, tail latency (p99), and allocation rate. Give it a couple of days, too — tail latency has a habit of showing up late.


What I like about this story is that there's no clever new algorithm in it. It's the same mark-and-sweep that's always been there. Someone just looked at how the hardware underneath actually behaves, and decided not to rush in the one place where everyone else was rushing.

And now you have something to say in an interview besides "it's 40% faster."


A question for you, and it's a real one

I teach a free Go internals course — the scheduler, escape analysis, the GC, interfaces, generics, context. This article started life as one lesson from it.

The catch: it's in Russian, so it's not much use to most of you.

I'm trying to work out whether it's worth rebuilding in English, and I'd rather ask than guess. That's a few months of work, so I'd like to know if anyone actually wants it before I start.

Two ways to tell me, both take a few seconds:

  1. Add your email to the waitlist — no course to sell, nothing to buy, no newsletter. One message if and when it exists, and that's it.
  2. Or just answer in the comments: which of these would you actually want to go deep on — the scheduler, escape analysis, generics internals, or context?

Honest answers help more than polite ones. If the answer is "there's already plenty of this in English," that's genuinely useful and I'd rather hear it now.

Sources

Top comments (0)