A deep dive into the Go memory allocator and garbage collector
about.sourcegraph.com
about.sourcegraph.com
FYI, this was part of a technical liveblog that covered all the talks at GopherCon this year in a similar fashion. If you missed the conference or are looking to review material, you might find https://sourcegraph.com/gophercon helpful. (Disclosure: I'm CTO of Sourcegraph—we organized the liveblog and love Go!)
Please quote where i said this. I said:
> for me
My requirements are not your requirements. Runtime pausing execution arbitrarily = bad for me.
I do also consider it a design failure to plug GC into what was originally marketed as a systems language.
Can this be detected statically?
It's easy enough to profile and benchmark in go, so I would always treat that as the source of truth.
Ergo, this value is safe to allocate on stack.
Go should switch to this strategy as well.
Furthermore, there isn't much of a relevant difference between stack allocated and nursery allocated objects, because they have to be scanned either way--either as roots or via a Cheney scan. The difference is only in sweeping, which is incredibly fast for nursery objects.
What's really important about generational GC is that heap allocation becomes nearly as cheap as stack allocation. That's a game changer.
Your comparison of heap allocation generational GC with stack allocation is not correct. Yes, allocating in the nursery is very fast, a couple of cycles only. The stack frame gets allocated once per function call. Yes, when a GC runs, you have to scan the whole heap, including the stack.
But what you are completely missing is, that allocating on the heap eventually triggers a GC, which takes cpu resources to perform. After a collection of the nursery, all surviving objects are promoted to the older generations. This promotion not only takes work, but grows the older generations which are more expensive to GC. So, while heap allocation with a generational GC is very cheap, it is not free. A large allocation count causes more frequent GC runs and objects might be promoted to older generations prematurly. As a consequence, a program that allocates less will perform better. Avoiding a high amount of heap allocations is a good way to increase your programs performance, be it by doing stack allocation, or by reusing buffers for example.
The reason was that they didn't have time to implement copying GC, per the talk. That's fair as far as engineering schedules are concerned. It says nothing about how good generational GC is in general.
> But what you are completely missing is, that allocating on the heap eventually triggers a GC, which takes cpu resources to perform.
It causes a minor GC only. Those are very cheap.
Yes, there are potential add-on costs. But it's been repeatedly shown that with a fast generational GC, the benefit of escape analysis for garbage collection is marginal. That's why Java HotSpot took so long to implement it. The main benefit of escape analysis in HotSpot, in fact, is that it allows SROA-like optimizations like lock elision, not that it makes garbage collection faster. Generational GCs really are that good.
Go allocations are indeed costlier but the performance critical sections of applications can be profiled and optimized accordingly to remove allocations.
I'd rather have Go's amazing low GC latency and slightly higher allocation costs vs the operational nightmare from HotSpot.
Automatic management of generations has never fully worked in Java. Every new JDK version just adds more knobs. Sounds like you have a different experience?
In other words, you're tuning to today's runtime if you reuse heavily. Those techniques may not age well, and will harm readability and maintainability.
It isn't, and will probably never be. The Go runtime developers have actually implemented and tested a generational GC, but found that it's not useful, and even harmful at times, to the goal of having fast, low-latency, concurrent garbage collector. Mostly because most short-lived objects are allocated on stack, and the escape analysis is getting better with each release.
This benefit is directly relevant to this thread, because bump allocation in the nursery makes the allocation fast path somewhere on the order of 6 instructions.
EDIT: Not sure why I'm being downvoted; if this is a contentious topic, I wasn't aware. I'm certainly not trying to make a point one way or another; I only posted this hoping someone who was more knowledgeable than me could respond.
2. Go does not have "a powerful GC", it has a GC built quite specifically for it and the applications it's intended to support with low-latency, middling throughput, and the assumption that allocations are long-lived (because short-lived objects don't escape and thus don't get heap-allocated)
3. Which is the issue of providing generic GC strategies: the type of GC you want and how you want to tune it is very much a factor of the language's semantics and its intended use cases, hence the official JVM shipping with something like half a dozen different GCs, and third-party JVMs having their own with their own tradeoffs
And when one have some (parts, modules, libraries) in your program which are best suited with different GC behaviour, things tend to become complicated. And cross-language boundaries is yet another talk.
2. The assumption that allocations are long-lived can often be made because a compiler can perform escape-analysis (and in fact LLVM could provide utilities for this).
3. Yes, I'm not saying that GoLang's GC (or an adaptation thereof) is the holy grail, just that it would be very useful for many other languages. And work in this area could help bring LLVM's suite of GCs to a competitive, world-class level.
That being said, the Bohem garbage collector tends to be what languages use until they can write their own.