Live data from Hacker News

Go Optimization Guide

goperf.dev

101–110 of 173 posts

Re: Go Optimization Guide

#101
post #73

Earlier quoted context omitted.

also no one GCs sync.Pool. After a spike in utilization, live with increased memory usage until program restart.

That's just not true. Pool contents are GCed after two cycles if unused.

What do you mean? Pool content can't be GCed , because there are references to it: pool itself.

What people do is what this article suggested, pool.Get/pool.Put, which makes it only grow in size even if load profile changes. App literally accumulated now unwanted garbage in pool and no app I have seen made and attempt to GC it.

Re: Go Optimization Guide

#102
post #94

Earlier quoted context omitted.

> What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does. One could define a new "Pool[T]" type (extending sync.Pool) to get these guarantees: type Pool[T any] sync.Pool // typed def func (p *Pool[T]) Get() T { // typed Get pp := (*sync.Pool)(p) return pp.Get().(T) } func (p *Pool[T]) Put(v T) { // typed Put pp := (*sync.Pool)(p) pp.Put(v) } intpool := Pool[…

> One could define a new "Pool[T]" type (extending sync.Pool) to get these guarantees: So long as that one is not you? You completely forgot to address the expectation: type Foo struct{ V int } pool := Pool[*Foo]{ // Your Pool type. New: func() any { return new(Foo) }, } a := pool.Get() a.V = 10 pool.Put(a) b := pool.Get() fmt.Println(b.V) // Prints: 10; 0 was expected.

> You completely forgot to address the expectation

> fmt.Println(b.V) // Prints: 10; 0 was expected.

Sorry, I don't get what else one expects when pooling pointers to a type? In fact, pooling *[]uint8 or *[]byte is common place; Pool.Put() or Pool.Get() then must zero its contents.

Re: Go Optimization Guide

#103
post #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…

I enjoyed your detailed response, it adds value to this discussion, but I feel you missed the point of my comment.

I am against blanket statements "reduce allocations to reduce GC pressure", which lead people wrong way: they compare libraries based on "allocs/op" from go bench, they trust rediculous (who allocates 8KB per iteration in tight loop??) microbenchmarks of sync.Pool like in the article above, hoping to resolve their GC problem. Spend considerabe amount of effort just to find that they barely moved a needle on GC times.

If we generalize then my "avoid long-lived allocations" or yours "reduce allocation rate in terms of bytes" are much more useful in practice, than what this and many other articles preach.

Re: Go Optimization Guide

#104
post #69
post #31

Earlier quoted context omitted.

If you have a moving, generational GC, then all the benefits of fast allocation are lost due to data moving and costly memory barriers.

Not at all. Most objects die young and thus are never moved. Also, the time before it is moved is very long compared to CPU operations so it is only statistically relevant (very good throughput, rare, longer tail on latency graphs). Also, write-only barriers don't have that big of an overhead.

It doesn't matter if objects die young — the other objects on the heap are still moved around periodically, which reduces performance. When you're using a moving GC, you also have additional read barriers that non-moving GCs don't require.

Re: Go Optimization Guide

#105

Earlier quoted context omitted.

Are you including in this analysis the amount of time/resources it takes to allocate? GC isn't the only thing you want to minimize for when you're making a high performance system.

From that perspective it boils down to "do less", which is what any perf guide already includes, allocations is just no different from anything else what app do. My comment is more about "reduce allocations to reduce GC pressure" advice seen everywhere. It doesn't tell the whole story. Short lived allocation doesn't introduce any GC pressure: you'll be hard pressed to see GC sweep phase on pprof without zooming. Peop…

Yeah I understand what you’re saying, but my point is you’re doing the opposite side of the same coin. Not doing full perf analysis and saying this one method works (yours is to reduce GC mark time, ignoring allocation, others are trying to reduce allocation time, ignoring GC time, or all these other methods listed in this doc.)

Re: Go Optimization Guide

#106
post #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.

A new sync/v2 NewPool() is being discussed that eliminates the sharp edges by making it generic: https://github.com/golang/go/issues/71076

I haven't personally found it to be problematic; just keep it private, give it a default new func, and be cautious about only putting things in it that you got out.

Re: Go Optimization Guide

#107
post #94

Earlier quoted context omitted.

> One could define a new "Pool[T]" type (extending sync.Pool) to get these guarantees: So long as that one is not you? You completely forgot to address the expectation: type Foo struct{ V int } pool := Pool[*Foo]{ // Your Pool type. New: func() any { return new(Foo) }, } a := pool.Get() a.V = 10 pool.Put(a) b := pool.Get() fmt.Println(b.V) // Prints: 10; 0 was expected.

> You completely forgot to address the expectation > fmt.Println(b.V) // Prints: 10; 0 was expected. Sorry, I don't get what else one expects when pooling pointers to a type? In fact, pooling * []uint8 or * []byte is common place; Pool.Put() or Pool.Get() then must zero its contents.

> I don't get what else one expects when pooling pointers to a type?

As seen in your previous comment, the expectation is that the zero value will always be returned: "What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does." To which you offered a guarantee.

> Pool.Put() or Pool.Get() then must zero its contents.

Right. That is the solution (as was also echoed in the top comment in this thread) if one needs that expectation to hold. But you completely forgot to do it, which questions what your code was for? It behaves exactly the same as sync.Pool itself... And, unfortunately, doesn't even get the generic constraints right, as demonstrated with the int and bool examples.

Re: Go Optimization Guide

#108

nicely organised. I feel like this could grow into community driven current state-of-the-art of optimisation tips for Go. just need to allow people edit/comment their input easily (preferably in-place). I see there is github repo, but my bet people would not actively add their input/suggestions/research there, it is hidden too far from the content/website itself

For sure. Feels like the broader dev community could use a generic wiki platform like this, where every language or toolkit can have its own section. Not just for performance/optimization, but also for idiomatic ways to use a language in practice.

Re: Go Optimization Guide

#109
post #96
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` (…

It is fairly common your type system ends up with escape hatches allowing you to violate the type rules in practice. See e.g., OCaml and the function "magic" in the Obj module. It serves as a way around a limitation in the type system which you don't want to deal with. You can still have the rest of the code base be safe, as long as you create a wrapper which is. The same can be said about having imperative implement…

Obviously every type system in practice has escape hatches. But I’ve never seen another staticly-typed language where you need to break out of the type system so regularly.

Go’s type system has your back when you’re writing easy stuff.

But it throws up its hands and leaves you to fend for yourself when you need to do nearly anything interesting or complex, which is precisely when I want the type system to have my back.

I should not have to worry (or worse, not worry and be caught off guard) that my pool of database connections suddenly starts handing back strings.

Re: Go Optimization Guide

#110
post #107

Earlier quoted context omitted.

> You completely forgot to address the expectation > fmt.Println(b.V) // Prints: 10; 0 was expected. Sorry, I don't get what else one expects when pooling pointers to a type? In fact, pooling * []uint8 or * []byte is common place; Pool.Put() or Pool.Get() then must zero its contents.

> I don't get what else one expects when pooling pointers to a type? As seen in your previous comment, the expectation is that the zero value will always be returned: "What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does." To which you offered a guarantee. > Pool.Put() or Pool.Get() then must zero its contents. Right. That is the solution (as was also echo…

> And, unfortunately, doesn't even get the generic constraints right, as demonstrated with the int and bool examples.

If those constraints don't hold (like you say) it should manifest as runtime panic, no?

> What GP seems to expect is that sync.Pool() would always return a zeroed structure

Ah, well. You gots to be careful when Pooling addresses.

> But you completely forgot to do it, which questions what your code was for?

OK. If anyone expects zero values for pointers, then the New func should return nil (but this is almost always useless), or if one expects values to be zeroed-out, then Pool.Get/Put must zero it out. Thanks for the code review.

Post reply on HN