Why? It is the same as in C.
#include <stdio.h>
#include <stdlib.h>
struct slice {
int *data;
size_t len;
size_t cap;
};
struct slice readLogsFromPartition() {
int *data = malloc(2);
data[0] = 1;
data[1] = 2;
return (struct slice){ data, 2, 2 };
}
int main() {
struct slice s = readLogsFromPartition();
for (int i = 0; i < s.len; i++) {
printf("%d\n", s.data[i]);
}
free(s.data);
} func foo() {
x := []int { 1 }
//SNIP
}
Could translate to C either as: void foo() {
int* x = malloc(1 * sizeof(int));
x[0] = 1;
//...
}
Or as void foo() {
int data[1] = {1};
int *x = data;
//...
}
Depending on the content of //SNIP. However, some people think that the semantics can also match the semantics of the second version in C - when in fact the semantics of the Go code always match the first version, even when the actual implementation is the second version.* Of course, in reality, Java also does escape analysis to allocate on the stack, though it's less likely to happen because of the lack of value types.
int *foo(void) {
int x = 99;
return &x; // bad idea
}
vs. func foo() *int {
x := 99
return &x // fine
}
They think that Go, like C, will allocate x on the stack, and that returning a pointer to the value will therefore be invalid.(Pedants: I'm aware that the official distinction in C is between automatic and non-automatic storage.)
What you wrote is not the same in C and Go, because GC and escape analysis. But 9rx is also correct that what OP wrote is the same in C and Go.
So OP almost learned about escape analysis, but their example didn't actually do it. So double confusion on their side.
Escape analysis is the reason your `x` is on the heap. Because it escaped. Otherwise it'd be on the stack.[1]
Now if by "semantics of the code" you mean "just pretend everything is on the heap, and you won't need to think about escape analysis", then sure.
Now in terms of what actually happens, your code triggers escape analysis, and OP does not.
[1] Well, another way to say this I guess is that without escape analysis, a language would be forced to never use the stack.
func pointless() byte {
var a byte[1024]
a[0] = 1
return a[0]
}
That’s entirely up to the compiler, not something that’s determined by the language semantics. It could vary from platform to platform or compiler version to compiler version. So clearly you don’t need to think about the details of escape analysis to understand what your code does because in many cases you simply won’t know if your value is on the stack or not.Like any optimization, it makes sense to talk about what "will" happen, even if a language (or a specific compiler) makes no specific promises.
Escape analysis enables an optimization.
I think I understand you to be saying that "escape analysis" is not why returning a pointer to a local works in Go, but it's what allows some variables to be on the stack, despite the ability to return pointers to other "local" variables.
Or similar to how the compiler can allow "a * 6" to never use a mul instruction, but just two shifts and an add.
Which is probably a better way to think about it.
> So clearly you don’t need to think about the details of escape analysis to understand what your code does
Right. To circle back to the context: Yeah, OP thought this was due to escape analysis, and that's why it worked. No, it's just a detail about why other code does something else. (but not really, because OP returned the slice by value)
So I suppose it's more correct to say that we were never discussing escape analysis at all. An escape analysis post would be talking about allocation counts and memory fragmentation, not "why does this work?".
Claude (per OPs post) led them astray.
Assuming to everything allocates on the heap, will solve this specific confusion.
My understanding is that C will let you crash quite fast if the stack becomes too large, go will dynamically grow the stack as needed. So it's possible to think you're working on the heap, but you are actually threshing the runtime with expensive stack grow calls. Go certainly tries to be smart about it with various strategies, but a rapid stack grow rate will have it's cost.
The initial stack size seems to be 2kb, a more on a few systems. So far I understand you can allocate a large local i.e. 8kb, that doesn't escape and grow the stack immediately. (Of course that adds up if you have a chain of calls with smaller allocs). So recursion is certainly not the only concern.
I am pretty sure the escape analysis doesn't affect the initial stack size. Escape analysis does determine where an allocation lives. So if your allocation is lower then what escape analysis considers heap and bigger then the initial stack size, the stack needs to grow.
What I am certain about, is that I have runtime.newstack calls accounting for +20% of my benchmark times (go testing). My code is quite shallow (3-4 calls deep) and anything of size should be on the heap (global/preallocated) and the code has zero allocations. I don't use goroutines either, it might me I still make a mistake or it's the overhead from the testing benchmark. But this obviously doesn't seem to be anything super unusual.
Depends what you mean by “large”. As of 1.24 Go will put slices several KB into the stack frame:
make([]byte, 65536)
Goes on the stack if it does not escape (you can see Go request a large stack frame) make([]byte, 65537)
goes on the heap (Go calls runtime.makeslice).Interestingly arrays have a different limit: they respect MaxStackVarSize, which was lowered from 10MB to 128 KB in 1.24.
If you use indexed slice literals gc does not even check and you can create megabyte-sized slices on the stack.
// -smallframes
// ir.MaxStackVarSize = 64 * 1024
// ir.MaxImplicitStackVarSize = 16 * 1024
a := [64 * 1024 +1]byte{}
b := make([]byte, 0, 16 * 1024 +1)
// default
// MaxStackVarSize = int64(128 * 1024)
// MaxImplicitStackVarSize = int64(64 * 1024)
c := [128 * 1024 +1]byte{}
d := make([]byte, 0, 64 * 1024 +1)
Not sure how to verify this, but the assumption you can allocate megabytes on the stack seems wrong. The output of the escape analysis for arrays is different then the make statement: test/test.go:36:2: moved to heap: c
Maybe an overlook because it is a bit sneaky? []byte{N: 0}And creates an on-stack slice whose size is only limited by Go's 1GB limit on individual stack frames: https://godbolt.org/z/rKzo8jre6 https://godbolt.org/z/don99e9cn
Interesting, [...] syntax works here as expected. So escape analysis simply doesn't look at the element list.