4 ms·
Yes, it's optimized for latency: https://tip.golang.org/doc/gc-guide#Latency https://tip.golang.org/doc/gc-guide#Latency So, for example, network code gets bet
by hu3 2y ago
Yes, it's optimized for latency:
https://tip.golang.org/doc/gc-guide#Latency https://tip.golang.org/doc/gc-guide#Latency
So, for example, network code gets better p99.
This, along with other sensible tradeoffs, is why Go ate a large slice of network software market.
- kgeist 2y agoIIRC Go has "GC assist" which forces goroutines that allocate too frequently to assist in GC work (share CPU time, i.e. they're slowed down), and I've never seen anything like that described for Java/C#. Interesting approach.
- gf000 2y agoIn other words, it slows down the user's code randomly. I wouldn't call that a necessarily good tradeoff.
- kgeist 2y agoIt's a back-pressure mechanism. I think slightly slower code on average is better than unexpected long GC pauses.
- gf000 2y agoBut an important code that has to allocate a lot will be disproportionately affected by this. Also, it doesn't solve "unexpectedly long GC pauses" just somewhat lowers their frequency. Java's ZGC is multiple generations ahead, and I believe is a fairer tradeoff (overall lower throughput (due to read barriers over write barriers), but guaranteed <1ms pause time due to basically everything happening concurrently)
- kgeist 2y agoGo's garbage collector is concurrent, too. GC assist makes sure the GC can keep up with allocations.
- gf000 2y agoCorrect me if I'm wrong, but concurrent GC only means that certain phases of GC can be done in parallel. GC pauses are a necessity exactly for the part that can't be done in parallel, and Go is definitely not using ZGC's next-generation design that would decouple the pause's length from the heap size, so we still have our "arbitrary long pause" problem on the table.
- kgeist 2y agoFrom Go's docs: >The Go GC avoids making the length of any global application pauses proportional to the size of the heap, and that the core tracing algorithm is performed while the application is actively executing. >Brief stop-the-world pauses when the GC transitions between the mark and sweep phases. I didn't benchmark it myself, though. Go is mostly used in stateless microservices where huge heap sizes are very uncommon. We usually scale horizontally, not vertically. Also, Go has value objects which can be allocated on the stack or embedded inside other objects, so it allocates far less garbage than Java. So practically for me Go's GC has been a non-issue, I barely remember it even exists.
- yvdriess 2y agoGo GC's stop the world pauses are essentially synchronization barriers at the start and end of a GC phase. That pause length is a function of the number of worker threads, not the size of the heap. You can check this yourself by running the tile38 and cockroachdb benchmarks with GODEBUG=gctrace=1 .
- gf000 2y agoIt's the function of both, as per even the sibling comment. Obviously there is not much to synchronize when there is only a single thread.
- yvdriess 2y agoAny concurrent GC will have an unpredictable impact on application threads. The more obvious is the necessary synchronization on GC startup and shutdown, e.g. to turn on write barriers during GC. Less obvious but perhaps more impactful is the cache trashing incurred by the GC threads' heap traveral.
- gf000 2y ago"Optimized" as in stops the user thread from doing useful work, when a lot of allocation happens? At least a couple of years ago Go had a very simplistic GC, but even today it is absolutely nowhere close to how good Java's GCs are (there is ZGC, for example, whose pause times are completely decoupled from heap size, so it can actually keep sub-millisecond pause times - the OS causes bigger pauses than that). At most Go puts slightly less work on its GC due to having value types. Sure, it's a "microbenchmark", but it might be eye-opening how big the difference is: https://benchmarksgame-team.pages.debian.net/benchmarksgame/performance/binarytrees.html https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
- neonsunset 2y agoBinary-trees is very interesting. Showcases how Java can easily compete with native code that uses hand-managed arena allocators. While investigating why it is so much better than .NET* in this case I reached a conclusion that one of the major contributing factors is that it uses TLAB which "inlines" the GC allocation code completely into the callers, making the allocations indeed just thread-local pointer bumps. .NET has something similar (allocation context) but you do have to go through a call. I assume TLAB allocations and TLAB refills are just this much better regardless. (* in all the number crunching ones we have every other GC language, including Java and Go, confidently beat <3 )
- gf000 2y agoYeah, it's probably a bit unfair given that GC research itself is just Java. Also, there might be some philosophical difference at play here, Java (the JVM) tends to expose only a very limited API (no structs, pointers, etc), but this allows more flexibility on the runtime part, while .NET let's the developer touch/control everything, but that might means less room for doing some advanced stuff on the runtime side. The age-old generic-specific balance. (Also, I like this playful competition between languages, much better than pointless flamewars :D)
- mknyszek 2y agoLooking at only the CPU numbers from this benchmark is misleading. This site requires the use of default configurations for each language runtime, and JVMs tend to have a much larger default heap than the Go runtime. Tracing GCs tend to have a CPU/memory tradeoff built into them [1]. Compare the memory footprint of the best Go and best Java programs in terms of wall time [2] (the site doesn't make it easy, you have to go back and forth between the two links) and the difference is enormous (these Go programs are running with much smaller total heap sizes, so much less runway for the GC). If you use GOGC and GOMEMLIMIT to even the playing field (and note, use a Go program that isn't using sync.Pool) the difference in wall time is far less stark (though it's still there, maybe 5-15%; don't quote me, it's been a long time since I measured this and I don't remember exactly). (The difference in total CPU time is bigger.) And finally, keep in mind this benchmark is hammering as hard as it can on the GC. How it impacts real applications depends on how much the application relies on the heap. [1] https://go.dev/doc/gc-guide#Understanding_costs https://go.dev/doc/gc-guide#Understanding_costs [2] https://benchmarksgame-team.pages.debian.net/benchmarksgame/performance/binarytrees-mem.html https://benchmarksgame-team.pages.debian.net/benchmarksgame/...