Live data from Hacker News

Go Optimization Guide

goperf.dev

91–100 of 173 posts

Re: Go Optimization Guide

#91

Earlier quoted context omitted.

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. S…

Long before sync.Pool was a thing, I wrote a pool for []bytes: https://github.com/thejerf/gomempool I haven't taken it down because it isn't obsoleted by sync.Pool because the pool is aware of the size of the []bytes. Though it may be somewhat obsoleted by the fact the GC has gotten a lot better since I wrote it, somewhere in the 1.3 time frame. But it solve exactly that problem I had; relatively infrequent messages from the computer's point of view (e.g., a system that is probably getting messages every 50ms or so), but that had to be pulled into buffers completely to process, and had highly irregular sizes. The GC was doing a ton of work when I was allocating them all the time but it was easy to reuse buffers in my situation.

Re: Go Optimization Guide

#92

Earlier quoted context omitted.

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

Basically, anything that isn't a thin pointer (*T, chan, map) gets boxed nowadays. The end result is that both words of an interface value are always pointers [1], which is very friendly to the garbage collector (setting aside the extra allocations when escape analysis fails). I've seen some tricks in the standard library to avoid boxing, e.g. how strings and times are handled by log/slog [2].

[1]: https://github.com/teh-cmc/go-internals/blob/master/chapter2...

[2]: https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/...

Re: Go Optimization Guide

#93
post #52

Earlier quoted context omitted.

> That's what the New function does, isn't it? But that's only run when the pool needs to allocate more space. What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does. I think Golang's implementation does make sense, as sync.Pool() is clearly an optimization you use when performance is an issue; and in that case you almost certainly want to only initialize pa…

> 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[…

[deleted]

Re: Go Optimization Guide

#94
post #52

Earlier quoted context omitted.

> That's what the New function does, isn't it? But that's only run when the pool needs to allocate more space. What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does. I think Golang's implementation does make sense, as sync.Pool() is clearly an optimization you use when performance is an issue; and in that case you almost certainly want to only initialize pa…

> 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.

Re: Go Optimization Guide

#95
post #50
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` (…

You never programmed in Go, I assume? Then you have to understand that the type of `pool.Get()` is `any`, the wildcard type in Go. It is a type, and if you want the underlying value, you have to get it out by asserting the correct type. This cannot be solved with generics. There's no way in Java, Rust or C++ to express this either, unless it is a pool for a single type, in which case Go generics indeed could handle t…

> There's no way in Java, Rust or C++ to express this either

You make it look like it's a good thing to be able to express it.

There's no way in Java, Rust or C++ to express this, praised be the language designers.

As for expressing a pool value that may be multiple things without a horrible any type and an horrible cast, you could make an union type in Rust, or an interface in Java implemented by multiple concrete objects. Both ways would force the consumer to explicitly check the value without requiring unchecked duck typing.

Re: Go Optimization Guide

#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 implementations with functional interfaces wrapping said implementation. From the outside, you have a view of a system which is functionally sound. Internally, it might break the rules and use imperative code (usually for the case of efficiency).

Re: Go Optimization Guide

#97

Earlier quoted context omitted.

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

go1.15 re-added small integer packing into interfaces: https://go.dev/doc/go1.15#runtime

Re: Go Optimization Guide

#98

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…

Agree that mark phase is the expensive bit. Disagree that it’s not worth reducing short-lived allocations. I spend a lot of time analyzing Go program performance, and reducing bytes allocated per second is always beneficial.

+1. In particular []byte slice allocations are often a significant driver of GC pace while also being relatively easy to optimize (e.g. via sync.Pool reuse).

Re: Go Optimization Guide

#99
post #92

Earlier quoted context omitted.

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

Basically, anything that isn't a thin pointer (*T, chan, map) gets boxed nowadays. The end result is that both words of an interface value are always pointers [1], which is very friendly to the garbage collector (setting aside the extra allocations when escape analysis fails). I've seen some tricks in the standard library to avoid boxing, e.g. how strings and times are handled by log/slog [2]. [1]: https://github.com…

[deleted]

Re: Go Optimization Guide

#100

Earlier quoted context omitted.

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

go1.15 re-added small integer packing into interfaces: https://go.dev/doc/go1.15#runtime

It didn't, actually. Instead go 1.15 has a static array of the first 256 positive integers, and when it needs to box one for an interface it gets a pointer into that array instead: https://go-review.googlesource.com/c/go/+/216401/4/src/runti...

This array is also used for single-byte strings (which previously had its own array): https://go-review.googlesource.com/c/go/+/221979/3/src/runti...

Post reply on HN