Live data from Hacker News

Go's escape analysis and why my function return worked

bonniesimon.in

61–70 of 76 posts

Re: Go's escape analysis and why my function return worked

#61
post #58
post #47

Earlier quoted context omitted.

What confuses people is 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.)

Yes. That's escape analysis. But this is not what OP did. 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.

Well, my point is that escape analysis has nothing to do with it at the semantic level. So it's actually just 'because GC'. You don't need the concept of escape analysis at all to understand the behavior of the Go example.

Re: Go's escape analysis and why my function return worked

#62
post #55

Earlier quoted context omitted.

I think we mix things up here. But be aware of my newbie knowledge. 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…

I don't know about your code, but in general, goroutine stacks are designed to start small and grow. There is nothing concerning about this. A call to runtime.newstack triggered by a large stack-allocated value would generally be cheaper than the corresponding heap allocation.

I found my issue, I was creating a 256 item fixed array of a 2*uint8 struct in my code. That was enough to cause newstack calls. It now went down from varying 10% to roughly 1%. Oddly enough it didn't change the ns/op a bit. I guess some mix of workload related irrelevancy and inaccurate reporting or another oversight on my side.

Re: Go's escape analysis and why my function return worked

#63

Earlier quoted context omitted.

The confusion begins the moment you think Go variables get allocated on the stack, in the C sense. They don't, semantically. Stack allocation is an optimization that the Go compiler can sometimes do for you, with no semantics associated with it. The following Go code also works perfectly well, where it would obviously be UB in C: func foo() *int { i := 7 return &i } func main() { x := foo() fmt.Printf("The int was: %…

Is that the case? I thought that it would be a copy instead of a heap allocation. Of course the compiler could inline it or do something else but semantically its a copy.

A copy of what? It’s returning a pointer, so i has to be on the heap[0].

gc could create i on the stack then copy it to the heap, but if you plug that code into godbolt you can see that it is not that dumb, it creates a heap allocation then writes the literal directly into that.

[0] unless Foo is inlined and the result does not escape the caller’s frame, then that can be done away with.

Re: Go's escape analysis and why my function return worked

#64
post #21

Earlier quoted context omitted.

I am currently learning go and your comment made me sort some things out, but probably in a counterintuitive way. 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…

Go won’t put large allocations on the stack even if escape analysis would permit it, so generally speaking this should only be a concern if you have very deep recursion (in which case you might have to worry about stack overflows anyway).

> Go won’t put large allocations on the stack even if escape analysis would permit it

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.

Re: Go's escape analysis and why my function return worked

#65
post #61
post #58

Earlier quoted context omitted.

Yes. That's escape analysis. But this is not what OP did. 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.

Well, my point is that escape analysis has nothing to do with it at the semantic level. So it's actually just 'because GC'. You don't need the concept of escape analysis at all to understand the behavior of the Go example.

Yeah. That's what I said.

Re: Go's escape analysis and why my function return worked

#66
post #65
post #61

Earlier quoted context omitted.

Well, my point is that escape analysis has nothing to do with it at the semantic level. So it's actually just 'because GC'. You don't need the concept of escape analysis at all to understand the behavior of the Go example.

Yeah. That's what I said.

I mean that escape analysis has nothing to do with my example either, in terms of understand the semantics of the code (so I’m disagreeing with the ‘because GC and escape analysis’ part of your comment).

Re: Go's escape analysis and why my function return worked

#67
post #56

Earlier quoted context omitted.

Depending on escape analysis, the array underlying the slice can get allocated on the stack as well, if it doesn't escape the function context. Of course, in this case, because we are returning a pointer to it via the slice, that optimization isn't applicable.

Agreed that it could in principle. But I can't immediately get it to do so: https://go.dev/play/p/9hLHattS8cf Both arrays in this example seem to be on the heap.

Taking the address of those variables makes them escape to heap. Even sending them to the Printf function makes them escape to heap.

If you want to confirm, you have to use the Go compiler directly. Take the following code:

  package main
  import (
    "fmt"
  )
  type LogEntry struct {
    s string
  }
  func readLogsFromPartition(partition int) []LogEntry {
    var logs []LogEntry // Creating an innocent slice
    logs = []LogEntry{{}}
    logs2 := []LogEntry{{}}
    fmt.Printf("%v %v\n", len(logs), len(logs2))
    return []LogEntry{{}}
  }
  func main() {
    logs := readLogsFromPartition(1)
    fmt.Printf("%p\n", &logs[0])
  }
And compile it with

  $ go build -gcflags '-m' main.go
  # command-line-arguments
  ./main.go:15:12: inlining call to fmt.Printf
  ./main.go:21:12: inlining call to fmt.Printf
  ./main.go:13:19: []LogEntry{...} does not escape
  ./main.go:14:21: []LogEntry{...} does not escape
  ./main.go:15:12: ... argument does not escape
  ./main.go:15:27: len(logs) escapes to heap
  ./main.go:15:38: len(logs2) escapes to heap
  ./main.go:16:19: []LogEntry{...} escapes to heap
  ./main.go:21:12: ... argument does not escape
However, if you return logs2, or if you take the address, or if you pass them to Printf with %v to print them, you'll see that they now escape.

An additional note: in your original code from your initial reply, everything you allocate escapes to heap as well. You can confirm in a similar way.

Re: Go's escape analysis and why my function return worked

#68
post #21

Earlier quoted context omitted.

Go won’t put large allocations on the stack even if escape analysis would permit it, so generally speaking this should only be a concern if you have very deep recursion (in which case you might have to worry about stack overflows anyway).

> Go won’t put large allocations on the stack even if escape analysis would permit it 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 MaxStac…

There is a option -smallframes that seems to be intended for conservative use cases. Below are the related configs and a test at what point they escape (+1).

  // -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?

Re: Go's escape analysis and why my function return worked

#69
post #66
post #65

Earlier quoted context omitted.

Yeah. That's what I said.

I mean that escape analysis has nothing to do with my example either, in terms of understand the semantics of the code (so I’m disagreeing with the ‘because GC and escape analysis ’ part of your comment).

Your https://news.ycombinator.com/item?id=46234206 relies on escape analysis though, right?

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.

Re: Go's escape analysis and why my function return worked

#70
post #69
post #66

Earlier quoted context omitted.

I mean that escape analysis has nothing to do with my example either, in terms of understand the semantics of the code (so I’m disagreeing with the ‘because GC and escape analysis ’ part of your comment).

Your https://news.ycombinator.com/item?id=46234206 relies on escape analysis though, right? 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 analys…

Escape analysis clearly isn’t part of the semantics of Go. For that to be the case, the language standard would have to specify exactly which values are guaranteed to be stack allocated. In reality, this depends on size thresholds which can vary from platform to platform or between different versions of the Go compiler. Is the following non-escaping array value stack allocated?

    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.
Post reply on HN