Live data from Hacker News

Go Optimization Guide

goperf.dev

31–40 of 173 posts

Re: Go Optimization Guide

#31
post #19

Earlier quoted context omitted.

Aren't allocations themselves pretty expensive regardless of GC?

No. If you have a moving multi generational GC, allocation is literally just an increment for short lived objects.

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

Re: Go Optimization Guide

#32
post #20

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…

Pretty similar story in .NET. Make sure your inner loops are allocation-free, then ensure allocations are short-lived, then clean up the long tail of large allocations.

.NET is far more tolerant to high allocation traffic since its GC is generational and overall more sophisticated (even if at the cost of tail latency, although that is workload-dependent).

Doing huge allocations which go to LOH is quite punishing, but even substantial inter-generational traffic won't kill it.

Re: Go Optimization Guide

#33
post #14

Earlier quoted context omitted.

Why would Pool increase memory usage?

I guess if you allocate more than you need upfront that it could increase memory usage.

I don't get it. The pool uses weak pointers under the hood right? If you allocate too much up front, the stuff you don't need will get garbage collected. It's no worse than doing the same without a pool, right?

Re: Go Optimization Guide

#35

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…

Its worth calling out that abstractions can kill you in unexpected ways with go. Anytime you use an interface it forces a heap allocation, even if the object is only used read only and within the same scope. That includes calls to things like fmt.Printf() so doing a for loop that prints the value of i forces the integer backing i to be heap allocated, along with every other value that you printed. So if you helpfully…

I thought surely an integer could be inlined into the interface, I thought Go used to do that. But I tried it on the playground, and it heap allocates it:

https://go.dev/play/p/zHfnQfJ9OGc

Re: Go Optimization Guide

#36

Earlier quoted context omitted.

Its worth calling out that abstractions can kill you in unexpected ways with go. Anytime you use an interface it forces a heap allocation, even if the object is only used read only and within the same scope. That includes calls to things like fmt.Printf() so doing a for loop that prints the value of i forces the integer backing i to be heap allocated, along with every other value that you printed. So if you helpfully…

I thought surely an integer could be inlined into the interface, I thought Go used to do that. But I tried it on the playground, and it heap allocates it: https://go.dev/play/p/zHfnQfJ9OGc

Go did use to do that, it was removed years ago, in 1.4: https://go.dev/doc/go1.4#runtime

Re: Go Optimization Guide

#37

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.

Re: Go Optimization Guide

#40

You can often fool yourself by using sync.Pool. pprof looks great because no allocs in benchmarks but memory usage goes through the roof. It's important to measure real world benefits, if any, and not just synthetic benchmarks.

Why would Pool increase memory usage?

Let's say you have constantly 1k requests per second and for each request, you need one buffer, each 1 MiB. That means you have 1 GiB in the pool. Without a pool, there's a high likelihood that you're using less. Why? Because in reality, most requests need a 1 MiB buffer but SOME require a 5 MiB buffer. As such, your pool grows over time as you don't have control over the distribution of the size of the pool items.

So, if you have predictable object sizes, the pool will stay flat. If the workloads are random, you have a new problem because, like in this scenario, your pool grows 5x more.

You can solve this problem. E.g. you can only give back items into the pool that are small enough. Alternatively, you could have a small pool and a big pool, but now you're playing cat and mouse.

In such a scenario, it could also work to simply allocate and use GC to clean up. Then you don't have to worry about memory and the lifetime of objects, which makes your code much simpler to read and reason about.

Post reply on HN