GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing go.dev — Allocating on the Stack - The Go Programming Language The Go BlogAllocating on the Stack
Keith Randall We’re always looking for ways to make Go programs faster. In the last 2 releases, we have concentrated on mitigating a particular source of slowness, heap allocations. Each time a Go program allocates memory from the heap, there’s a fairly large chunk of code that needs to run to satisfy that allocation. In addition, heap allocations present additional load on the garbage collector. Even with recent enhancements like Green Tea, the garbage collector still incurs substantial overhead. So we’ve been working on ways to do more allocations on the stack instead of the heap. Stack allocations are considerably cheaper to perform (sometimes completely free). Moreover, they present no load to the garbage collector, as stack allocations can be collected automatically together with the stack frame itself. Stack allocations also enable prompt reuse, which is very cache friendly. Stack allocation of constant-sized slicesConsider the task of building a slice of tasks to process:
Let’s walk through what happens at runtime when pulling tasks from the
channel On the first loop iteration, there is no backing store for On the second loop iteration, the backing store now exists, but it is
full. On the third loop iteration, the backing store of size 2 is
full. On the fourth loop iteration, the backing store of size 4 has only 3
items in it. On the fifth loop iteration, the backing store of size 4 is full, and
And so on. We generally double the size of the allocation each time it fills up, so we can eventually append most new tasks to the slice without allocation. But there is a fair amount of overhead in the “startup” phase when the slice is small. During this startup phase we spend a lot of time in the allocator, and produce a bunch of garbage, which seems pretty wasteful. And it may be that in your program, the slice never really gets large. This startup phase may be all you ever encounter. If this code was a really hot part of your program, you might be tempted to start the slice out at a larger size, to avoid all of these allocations.
This is a reasonable optimization to do. It is never incorrect; your
program still runs correctly. If the guess is too small, you get
allocations from If your guess for the number of tasks was a good one, then there’s
only one allocation site in this program. The The surprising thing is that if you benchmark this code with 10 elements in the channel, you’ll see that you didn’t reduce the number of allocations to 1, you reduced the number of allocations to 0! The reason is that the compiler decided to allocate the backing store
on the stack. Because it knows what size it needs to be (10 times the
size of a task) it can allocate storage for it in the stack frame of
Stack allocation of variable-sized slicesBut of course, hard coding a size guess is a bit rigid. Maybe we can pass in an estimated length?
This lets the caller pick a good size for the Unfortunately, in Go 1.24 the non-constant size of the backing store
means the compiler can no longer allocate the backing store on the
stack. It will end up on the heap, converting our 0-allocation code
to 1-allocation code. Still better than having But never fear, Go 1.25 is here! Imagine you decide to do the following, to get the stack allocation only in cases where the guess is small:
Kind of ugly, but it would work. When the guess is small, you use a
constant size But in Go 1.25, you don’t need to head down this ugly road. The Go
1.25 compiler does this transformation for you! For certain slice
allocation locations, the compiler automatically allocates a small
(currently 32-byte) slice backing store, and uses that backing store
for the result of the In Go 1.25, We’re always improving the performance of Go, so upgrade to the latest Go release and be surprised by how much faster and memory efficient your program becomes! Stack allocation of append-allocated slicesOk, but you still don’t want to have to change your API to add this weird length guess. Anything else you could do? Upgrade to Go 1.26!
In Go 1.26, we allocate the same kind of small, speculative backing
store on the stack, but now we can use it directly at the On the first loop iteration, there is no backing store for The next 3 loop iterations append directly to the stack backing store, requiring no allocation. On the 4th iteration, the stack backing store is finally full and we have to go to the heap for more backing store. But we have avoided almost all of the startup overhead described earlier in this article. No heap allocations of size, 1, 2, and 4, and none of the garbage that they eventually become. If your slices are small, maybe you will never have a heap allocation. Stack allocation of append-allocated escaping slicesOk, this is all good when the Right! The backing store for the slice returned by
But you might think, the returned slice can’t be allocated on the stack. But what about all those intermediate slices that just become garbage? Maybe we can allocate those on the stack?
Then the But do you really want to write all that additional code? It seems error prone. Maybe the compiler can do this transformation for us? In Go 1.26, it can! For escaping slices, the compiler will transform the original
This ensures that for our original The optimization that Go 1.26 does is actually better than the hand-optimized code, because it does not require the extra allocation+copy that the hand-optimized code always does at the end. It requires the allocation+copy only in the case that we’ve exclusively operated on a stack-backed slice up to the return point. We do pay the cost for a copy, but that cost is almost completely offset by the copies in the startup phase that we no longer have to do. (In fact, the new scheme at worst has to copy one more element than the old scheme.) Wrapping upHand optimization can still be beneficial, especially if you have a good estimate of the slice size ahead of time. But hopefully the compiler will now catch a lot of the simple cases for you and allow you to focus on the remaining ones that really matter. There are a lot of details that the compiler needs to ensure to get
all these optimizations right. If you think that one of these
optimizations is causing correctness or (negative) performance issues
for you, you can turn them off with
Footnotes1 Go stacks do not have any
Next article: //go:fix inline and the source-level inliner |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |