Live data from Hacker News

Go Optimization Guide

goperf.dev

81–90 of 173 posts

Re: Go Optimization Guide

#81
Unpopular opinion maybe, but sync.Pool is so sharp, dangerous and leaky that I'd avoid using it unless it's your absolute last option. And even then, maybe consider a second server first.

Re: Go Optimization Guide

#82

Every perf guide recommends to minimize allocations to reduce GC times, but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep. GC mark always starts with known live roots (goroutine stacks, globals, etc) and traverse references from there colouring every pointer. To minimize GC time it is best to avoid _long living_ allocations. Short lived allocations, those which GC mark phase will ne…

Side note: see https://tip.golang.org/doc/gc-guide for more on how the Go GC works and what triggers it.

GC frequency is directly driven by allocation rate (in terms of bytes) and live heap size. Some examples:

  - If you halve the allocation rate, you halve the GC frequency.
  - If you double the live heap size, you halve the GC frequency (barring changes away from the default `GOGC=100`).
> ...but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep.

It is true that sweeping is a lot cheaper than marking, which makes your next statement:

> Short lived allocations, those which GC mark phase will never reach, has almost neglible effect on GC times.

...technically correct. Usually, this is the best kind of correct, but it omits two important considerations:

  - If you generate a ton of short-lived allocations instead of keeping them around, the GC will trigger more frequently.
  - If you reduce the live heap size (by not keeping anything around), the GC will trigger more frequently.
So now you have cheaper GC cycles, but many more of them. On top of that, you have vastly increased allocation costs.

It is not a priori clear to me this is a win. In my experience, it isn't.

Re: Go Optimization Guide

#83

Every perf guide recommends to minimize allocations to reduce GC times, but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep. GC mark always starts with known live roots (goroutine stacks, globals, etc) and traverse references from there colouring every pointer. To minimize GC time it is best to avoid _long living_ allocations. Short lived allocations, those which GC mark phase will ne…

Is it worth making short lived allocations just to please the GC? You might just end up with too many allocations which will slow things down even more.

It is not. Please see my answer (https://news.ycombinator.com/item?id=43545500).

Re: Go Optimization Guide

#84

Earlier quoted context omitted.

While I think you're right (generics might be useful there), it's fairly easy to wrap the `sync` primitives such as `sync.Pool` and `sync.Map` into your specific use case. Go is pretty strict about breaking changes, so they probably won't change the current implementations; maybe we'll see a v2 version, or maybe not. The more code you have, the more code you have to maintain, and given Go's backward-compatibility pro…

Sync.map is meant to have poor performance I believe https://github.com/golang/go/issues/21031

It depends on the use case.

From the Godoc:

> The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.

Source: https://pkg.go.dev/sync#Map

And regarding slow writes, those were recently improved in Go 1.24:

> The implementation of sync.Map has been changed, improving performance, particularly for map modifications. For instance, modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads from the map.

Source: https://go.dev/doc/go1.24#minor_library_changes ("sync" section)

Re: Go Optimization Guide

#86

Can someone explain to me why the compiler can’t do struct-field-alignment? This feels like something that can easily be automated.

Because the order of fields can be significant. It's very relevant for syscalls, and is observable via the reflect package; it'd be strange if the field order was arbitrarily changed (and might change further between releases).

I assume the thinking was that this is pretty easy to optimise if you care, and if it's on by default there'd then have to be some opt-out which there isn't a good mechanism for.

Re: Go Optimization Guide

#87

Can someone explain to me why the compiler can’t do struct-field-alignment? This feels like something that can easily be automated.

Because the order of fields can be significant. It's very relevant for syscalls, and is observable via the reflect package; it'd be strange if the field order was arbitrarily changed (and might change further between releases). I assume the thinking was that this is pretty easy to optimise if you care, and if it's on by default there'd then have to be some opt-out which there isn't a good mechanism for.

In particular, struct field alignment matches C (even without cgo) and so any change to the default would break a lot of code.

Re: Go Optimization Guide

#88
post #76
post #15

Earlier quoted context omitted.

Go allocations aren't that bad. A few years ago I benchmarked them at about 4x as expensive as a bump allocation. That is slow enough to make an arena beneficial in high allocation situations, but fast enough to not make it worth it most of the time.

Comparing with a fairly optimized malloc at $COMPANY, the Go allocator is (both in terms of relative cycles and fraction of cycles of all Go programs) significantly more expensive than the C/C++ counterpart (3-4x IIRC). For one, it has to do more work, like setting up GC metadata, and zeroing. There have recently been some optimizations to `runtime.mallocgc`, which may have decrease that 3-4x estimate a bit.

How can that be true? If it is 3-4x more expensive than malloc, then per my measurements your malloc is a bump allocator, and that simply isn't true for any real world malloc implementation (typically a modified free list allocator afaik). `mallocgc` may not be fast, but I simply did not find it as slow as you are saying. My guess is it is about as fast as most decent malloc functions, but I have not measured, and it would be interesting to see a comparison (tough to do as you'd need to call malloc via CGo or write one in C and one in Go and trust the looping is roughly the same cost).

Re: Go Optimization Guide

#89
post #70
post #45

Checking out the first example—object pools—I was initially blown away that this is not only possible but it produces no warnings of any kind: pool := sync.Pool{ New: func() any { return 42 } } a := pool.Get() pool.Put("hello") pool.Put(struct{}{}) b := pool.Get() c := pool.Get() d := pool.Get() fmt.Println(a, b, c, d) Of course, the answer is that this API existed before generics so it just takes and returns `any` (…

This is still strong typing, even it it's not static typing. It's static vs. dynamic and strong vs. weak. https://stackoverflow.com/a/11889763

It is strong, static, and structural. But structural typing is effectively compile-time duck typing, so it is understandable that some might confuse it with dynamic typing.

Re: Go Optimization Guide

#90

Huh, this surprises me about Golang, didn't realise it was so similar to C with struct alignment. https://goperf.dev/01-common-patterns/fields-alignment/#why-...

Yup, it's a fairly low-level language intended as a replacement to C/C++ but for modern day systems (networked, concurrent, etc). You don't have manual memory management per se but you still need to decide on heap vs stack and consider the hardware.

"you still need to decide on heap vs stack"

No, you can't decide on heap vs stack. Go's compiler decides that. You can get feedback about the decision if you pass the right debug flags, and then based on that you may be able to tickle the optimizer into changing its mind based on code changes you make, but it'll always be an optimization decision subject to change without notice in any future versions of Go, just like any other language where you program to the optimizer.

If you need that level of control, Go is generally not the right language. However, I would encourage developers to be sure they need that level of control before taking it, and that's not special pleading for Go but special pleading for the entire class of "languages that are pretty fast but don't offer quite that level of control". There's still a lot of programmers running around with very 200x ideas of performance, even programmers who weren't programmers at the time, who must have picked it up by osmosis.

(My favorite example to show 200x perf ideas is paginated APIs where the "pages" are generally chosen from the set {25, 50, 100} for "performance reasons". In 2025, those are terribly, terribly small numbers. Presenting that many results to humans makes sense, but my default size for paginating API calls nowadays is closer to 1000, and that's the bottom end, for relatively expensive things. If I have no reason to think it's expensive, tack another order of magnitude on to my minimum.)

Post reply on HN