[HN Gopher] Allocating on the Stack
___________________________________________________________________
Allocating on the Stack
Author : spacey
Score : 104 points
Date : 2026-02-27 16:34 UTC (6 hours ago)
(HTM) web link (go.dev)
(TXT) w3m dump (go.dev)
| HarHarVeryFunny wrote:
| This article is about Go, but I wonder how many C/C++ developers
| realize that you've always had the ability to allocate on the
| stack using alloca() rather than malloc().
|
| Of course use cases are limited (variable length buffers/strings,
| etc) since the lifetime of anything on the stack has to match the
| lifetime of the stack frame (i.e the calling function), but it's
| super fast since it's just bumping up the stack pointer.
| ozgrakkurt wrote:
| This is more of a patch/hack solution as far as I can
| understand.
|
| You can just as well pass a heap allocated buffer + size around
| and allocate by incrementing/decrementing size.
|
| Or even better use something like zig's FixedSizeAllocator.
|
| Correct me if I am wrong please
| HarHarVeryFunny wrote:
| I wouldn't call it a hack, but it's not a general alternative
| for memory allocated on the heap since the lifetime is tied
| to that of the allocating function.
|
| I think what you're referring to is an arena allocator where
| you allocate a big chunk of memory from the heap, then
| sequentially sub-allocate from that, then eventually free the
| entire heap chunk (arena) in one go. Arena allocators are
| therefore also special use case since they are for when all
| the sub-allocations have the same (but arbitrary) lifetime,
| or at least you're willing to defer deallocation of
| everything to the same time.
|
| So, heap, arena and stack allocation all serve different
| purposes, although you can just use heap for everything if
| memory allocation isn't a performance issue for your program,
| which nowadays is typically the case.
|
| Back in the day when memory was scarce and computers were
| much slower, another common technique was to keep a reuse
| "free list" of allocated items of a given type/size, which
| was faster than heap allocate and free/coalesce, and avoided
| the heap fragmentation of random malloc/frees.
| rwmj wrote:
| Most C compilers let you use variable length arrays on the
| stack. However they're problematic and mature code bases
| usually disable this (-Werror -Wvla) because if the size is
| derived from user input then it's exploitable.
| stackghost wrote:
| alloca()'s availability and correctness/bugginess is platform
| dependent, so it probably sees only niche usage since it's not
| portable. Furthermore, even its man page discourages its use in
| the general case:
|
| >The alloca() function is machine- and compiler-dependent.
| Because it allocates from the stack, it's faster than malloc(3)
| and free(3). In certain cases, it can also simplify memory
| deallocation in applications that use longjmp(3) or
| siglongjmp(3). Otherwise, its use is discouraged.
|
| Furthermore:
|
| >The alloca() function returns a pointer to the beginning of
| the allocated space. _If the allocation causes stack overflow,
| program behavior is undefined._
|
| https://man7.org/linux/man-pages/man3/alloca.3.html
| spacechild1 wrote:
| alloca() is super useful, but it's also quite dangerous because
| you can easily overflow the stack.
|
| The obvious issue is that you can't know how much space is left
| on the stack, so you basically have to guess and pick an
| arbitrary "safe" size limit. This gets even more tricky when
| functions may be called recursively.
|
| The more subtle issue is that the stack memory returned by
| alloca() has function scope and therefore you must never call
| it directly in a loop.
|
| I use alloca() on a regular basis, but I have to say there are
| safer and better alternatives, depending on the particular use
| case: arena/frame allocators, threadlocal pseudo-stacks, static
| vectors, small vector optimizations, etc.
| 12_throw_away wrote:
| > The obvious issue is that you can't know how much space is
| left on the stack [...]
|
| Oh, huh. I've never actually tried it, but I always assumed
| it would be possible to calculate this, at least for a given
| OS / arch. You just need 3 quantities, right?
| `remaining_stack_space = $stack_address - $rsp -
| $system_stack_size`.
|
| But I guess there's no API for a program to get its own stack
| address unless it has access to `/proc/$pid/maps` or similar?
| chuckadams wrote:
| If your API includes inline assembly, then it's trivial.
| Go's internals would need it to swap stacks like it does.
| But I doubt any of that is exposed at the language level.
| Joker_vD wrote:
| > $system_stack_size
|
| Does such thing even exist? And non-64 bit platforms the
| address space is small enough that with several threads of
| execution you may just be unable to grow your stack even up
| to $system_stack_size because it'd bump into something
| else.
| masklinn wrote:
| > Does such thing even exist?
|
| AFAIK no. There are default stack sizes, but they're just
| that, defaults, and they can vary on the same system:
| main thread stacks are generally 8MiB (except for Windows
| where it's just 1) but the size of ancillary stacks is
| much smaller everywhere but on linux using glibc.
|
| It should be possible to get the stack root and size
| using `pthread_getattr_np`, but I don't know if there's
| anyone bothering with that, and it's a glibc extension.
| MarkSweep wrote:
| .NET bothers with it, to support
| RuntimeHelpers.EnsureSufficientExecutionStack [1] and
| other things. See the pthreads calls used to here [2].
|
| [1]: https://learn.microsoft.com/en-
| us/dotnet/api/system.runtime....
|
| [2]: https://github.com/dotnet/runtime/blob/b6a3e784f0bb4
| 18fd2fa7...
| fluntcaps wrote:
| You can do something like: void
| *get_sp(void) { volatile char c;
| return (void *)&c; }
|
| Or, in GCC and Clang: void *get_sp(void)
| { return __builtin_frame_address(0); }
|
| Which gets you close enough.
| wat10000 wrote:
| It's certainly possible on some systems. Even then, you
| have to fudge, as you don't know exactly how much stack
| space you need to save for other things.
|
| Stack memory is weird in general. It's usually a fixed
| amount determined when the thread starts, with the size
| typically determined by vibes or "seems to work OK." Most
| programmers don't have much of a notion of how much stack
| space their code needs, or how much their program needs
| overall. We know that unbounded non-tail recursion can
| overflow the stack, but how about bounded-but-large? At
| what point do you need to start considering such things? A
| hundred recursive calls? A thousand? A million?
|
| It's all kind of sketchy, but it works well enough in
| practice, I suppose.
| spacechild1 wrote:
| Personally, I only use alloca() if:
|
| 1. I know that the function will never be called
| recursively and
|
| 2. the total amount of stack allocation is limited to a
| few kilobytes at most.
|
| alloca() is more problematic on embedded platforms
| because default stack sizes tend to be tiny. Either
| document your stack usage requirements or provide an
| option to disable all calls to alloca(). For example,
| Opus has the OPUS_NONTHREADSAFE_PSEUDOSTACK option.
| norir wrote:
| If you have well defined boundaries, you can move the stack
| to an arbitrarily large chunk of memory before the recursive
| call and restore it to the system stack upon completion.
| chuckadams wrote:
| And if you never do reach completion, you can just garbage
| collect that chunk. AKA "Cheney on the MTA":
| https://dl.acm.org/doi/10.1145/214448.214454
| cyberax wrote:
| > alloca() is super useful, but it's also quite dangerous
| because you can easily overflow the stack.
|
| This is not a problem for Go, because it has resizable
| stacks.
| anematode wrote:
| If you're not doing recursion, I prefer using an appropriately
| sized thread_local buffer in this scenario. Saves you the
| allocation and does the bookkeeping of having one per thread
| lstodd wrote:
| It becames super slow when you bump that pointer into a page
| that's missing from the TLB.
| HarHarVeryFunny wrote:
| A TLB miss could happen when executing the next statement in
| your program. It's not something you have a lot of control
| over, and doesn't change the fact that allocating from the
| stack (when an option) is going to be faster than allocating
| from the heap.
| lstodd wrote:
| So you don't allocate left and right, be it stack or heap.
|
| It's all useless though unless you control the hardware. If
| you don't, you might as well prlimit --stack=unlimited and
| have at it.
| dzdt wrote:
| For purely historical reasons the C/C++ stack is "small" with
| exactly how small being outside of programmer control. So you
| have to avoid using the stack even if it would be the better
| solution. Otherwise you risk your program crashing/failing with
| stack overflow errors.
| csjh wrote:
| What do you mean outside of programmer control? What's
| stopping you from setting the stack size in the linker flags?
| HarHarVeryFunny wrote:
| With Linux the stack size is a process limit, set with
| ulimit (default 8MB?). You can even set it to unlimited if
| you want, meaning that essentially (but not quite) the
| stack and heap grow towards each other only limited by the
| size of the address space.
|
| ulimit only affects the main program stack though. if you
| are using multi-threading then there is a per-thread stack
| limit, which you can configure with pthreads, but not until
| C++23 for std::thread.
| bertylicious wrote:
| Nice! That's (seems) so simple yet also so very effective.
| Shouldn't other memory-managed languages be able to profit from
| this as well?
| lionkor wrote:
| C# has `stackalloc`
| bertylicious wrote:
| But that requires an explicit declaration and isn't done
| automatically under the hood, or am I missing something?
| Smaug123 wrote:
| The JIT does this automatically in some cases as of .NET 10
| (https://learn.microsoft.com/en-us/dotnet/core/whats-
| new/dotn...).
| nasretdinov wrote:
| Nice to see common and natural patterns to have their performance
| improved. Theoretically appending to a slice would be possible to
| handle with just stack growth, but that would require having
| large gaps between goroutine stacks and mapping them lazily upon
| access instead of moving goroutines to the new contiguous blocks
| as it's implemented right now. But given how many questionable
| changes it requires from runtime it's certainly not going to
| happen :)
| ivanjermakov wrote:
| Having big stack frames is bad for cache locality. Stack is not
| something magical, it's mapped to the same physical memory as
| heap and needs to be loaded. Pretty sure such optimization
| would reduce performance in most cases.
| wahern wrote:
| In the case where you're using the _top_ of the stack as a,
| well, stack, I don 't see the problem. It would only work if
| you're not interleaving processing of dynamically-sized
| objects and function codegen works out. It's similar to TCO
| in the sense of maintaining certain invariants across calls
| (e.g. no temporaries need be preserved), and actually in
| languages with TCO, like Lua, you can hack an application-
| level stack data structure using tail recursion (and
| coroutines/threads if you need more than one) that can
| sometimes be more performant or more convenient than using a
| native data structure.
|
| There's been a least one experiment (posted a few years ago
| to HN) where someone benchmarked a stackful coroutine
| implementation with hundreds of thousands (millions?) of
| stacks that could grow contiguously on-demand up to, e.g.,
| 2MB, but were initially minimally sized and didn't reserve
| the maximum stack size upfront. The bottleneck was the VMA
| bookkeeping--the syscalls, exploding the page table, TLB
| flushing, etc. In principle it could work well and be even
| more performant than existing solutions, and it might work
| better today since Linux 6.13's lightweight guard page
| feature, MADV_GUARD_INSTALL, but we probably still need more
| architectural support from the system (kernel, if not
| hardware) to make it performant and competitive with
| language-level solutions like goroutines, Rust async, etc.
| anematode wrote:
| Awesome stuff! Does Go have profile-guided optimization? I'm
| wondering whether a profile could hint to the compiler how large
| to make the pre-reserved stack space.
| tptacek wrote:
| Yep. `go build -pgo=foo.pprof`
|
| https://go.dev/doc/pgo
| karel-3d wrote:
| I never noticed much difference with using pgo even after
| taking a very long real life profile. All the machinery
| required to get it and put it to CI was never worth the
| speed-up. Of course YMMV.
| zabzonk wrote:
| alloca() is not part of the C++ standard, and I can't imagine how
| it could used safely in a C++ environment
| mwkaufma wrote:
| If I had a nickel for every article about avoiding implicit
| boxing in gc-heap languages...
| adonovan wrote:
| ...you would have the same balance as before, because this is
| not an article about implicit boxing. ;-)
| lstodd wrote:
| I read that as "Allocating on the Slack" and immediately came up
| with three ways how to do that.
| csjh wrote:
| Optimizations like these are so cool. I love seeing higher level
| languages take advantage of their high level-ness
| fsckboy wrote:
| you can do this in C, you just need to let its low level-ness
| be at the same level as everything else you do, just a setjmp
| longerjmp
| OptionOfT wrote:
| > ... > On the third loop iteration, the backing store of size 2
| is full. append again has to allocate a new backing store, this
| time of size 4. The old backing store of size 2 is now garbage.
|
| Correct me if I'm wrong, but isn't this a worst-case scenario?
| realloc can, iirc, extend in place. Your original pointer is
| still invalid then, but no copy is needed then.
|
| Unless I'm missing something?
|
| Equally, what happens to the ordering of variables on the stack?
| Is this new one pushed as the last one? Or is there space kept
| open?
|
| E.g.: var tasks []task var other_var
| int
| kbolino wrote:
| The ability to grow without copying is already part of how
| slices work. Every slice is really a 3-word tuple of pointer,
| length, _and capacity_. If not explicitly set with make, the
| capacity property defaults to a value that fills out the size
| class of the allocation. It just so happens that, in this case,
| the size of the Task type doesn 't allow for more than 1 value
| to fit in the smallest allocation. If you were to do this with
| a []byte or []int32 etc., you would see that the capacity
| doesn't necessarily start at 1:
| https://go.dev/play/p/G5cifdChGIZ
| matthewaveryusa wrote:
| It's kind of like the small string optimization you see in C++[1]
| where all the string metadata to account for heap pointer, size
| and capacity is union'ed with char*. Getting the stack allocation
| doesn't costs extra memory, but does cost a bit check. Not sure
| if slices in go use the same method. 32 bytes is a lot so maybe
| they fattened slice representations a bit to get a bit more bang
| for your buck?
|
| [1] https://github.com/elliotgoodrich/SSO-23
___________________________________________________________________
(page generated 2026-02-27 23:00 UTC)