UberBenchmark
Zero-Growth Stack, Real Gains: How Stack Allocation Can Save 10% CPU in Go
Summary
At Uber, repeated Go goroutine stack expansion consumed substantial CPU in a service whose stack-memory usage remained low, making memory a viable trade-off. Go starts goroutines with 2KB stacks and doubles them when needed, copying the previous stack; the post also notes that adaptive stack sizing and stack shrinking can add runtime cost. Uber patched the runtime, disabled adaptive sizing and shrinking, and used profile-guided analysis of pprof traces plus assembly-derived function stack usage to choose a static power-of-two starting size. For the profiled service, the analyzer identified about 19KB of required stack usage, leading to a 32KB setting instead of 2KB. After deployment, stack-growth CPU fell from close to 10% to below 1%, while most instances used about 50MB of stack and a few reached 200MB.
Context
Uber describes tuning Go goroutine stack sizes at large scale, where a 1% efficiency improvement across its services is worth several million dollars. The featured service had about 10% of CPU usage attributed to stack growth, while its instances had 16GB of memory and relatively low stack usage, leaving room to trade memory for CPU efficiency.
Approach / What changed
Uber patched the Go runtime to expose and modify the adaptive stack-start variables through private linking, disabled adaptive stack sizing and stack shrinking, and injected a static stack size through its configuration system. A profile-guided analyzer reads pprof traces, filters paths reaching copystack, extracts debug symbols, and examines function assembly to total stack usage. It selected the next power-of-two size, 32KB, for the profiled service; build tags and version checks reduce compatibility risk across Go releases.
Takeaways
Go goroutines begin with 2KB stacks and expand by allocating a stack twice as large and copying the previous contents, which can create significant CPU overhead.
Uber’s analyzer filters pprof traces reaching copystack and uses assembly instructions to calculate stack usage for each function and trace. A roughly 19KB requirement maps to a 32KB power-of-two starting stack.
Moving the service’s starting stack from 2KB to 32KB reduced stack-growth CPU from close to 10% to below 1%; most instances used about 50MB of stack, while a few reached 200MB.
FAQ
Why was increasing the starting stack size preferable for this service?
The service had 16GB of memory per instance and relatively low stack usage, so it had room to use more memory. That trade-off reduced repeated stack growth and its CPU cost.
How did the analyzer determine the 32KB setting?
It examined pprof stack traces that reached copystack, then calculated each function’s stack usage from assembly instructions. The highest analyzed trace used about 19KB, and the required setting was rounded up to the next power of two, 32KB.
What maintenance risk comes with changing the runtime?
The Go community could change the private definitions of the adaptive stack variables, causing the integration to stop working. Uber uses build tags and checks the runtime’s private structure on each new Go version to reduce the risk of crashes and other issues.
Source and freshness
- Source
- uber.com
- Published
- Added to Yomu
Summaries are generated from the original by a language model. Check the original before relying on it.
Keep reading
Related reading
The Good Stuff
Paid picks we use ourselves. Links carry a referral code — a subscription through one earns us a commission at no extra cost to you.
The best engineering reads, once a week
One useful email. Unsubscribe anytime.