-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
Context
Whenever a slice is allocated with a variable capacity (e.g. make([]int, 0, n)), the compiler concludes it escapes to the heap,
even though the slice does not actually escape.
Examples:
func sliceAllocInt(n int) int {
index := make([]int, 0) // does not escape, even though the append makes it allocate on the heap
for i := 0; i < n; i++ {
index = append(index, i)
}
return len(index)
}
func sliceAllocMakeInt(n int) int {
index := make([]int, n) // escapes, since n is unknown
for i := 0; i < n; i++ {
index[i] = i
}
return len(index)
}
func sliceAllocMakeConstInt(_ int) int {
const size = 32
index := make([]int, size) // does not escape: size is known at build time: correctly allocated to the stack
for i := 0; i < size; i++ {
index[i] = i
}
return len(index)
}Notice that this contrasts with escaping conclusions about similar constructs with maps:
- snippet (i) no escape, but will allocate on the heap because of the need to grow
- snippet (ii) escape (even if it actually doesn't), because the capacity to allocate is not known at build time
- snippet (iii): no escape & alloc on the stack as expected
Proposal
I propose to adopt a more aggressive strategy to allocate slices on the stack whenever possible,
whether the capacity is known at build time or not.
Snippet (ii) should be detected as no escape (I believe it is at some point, and we revert to escaping because of the
variable capacity). This would make the escape analysis consistent with maps.
The decision to allocate non-escaping slices to the stack or to the heap should be deferred to runtime,
favoring stack whenever the capacity fits and resorting to heap only for the larger slices.
At the very least, this should favor well-abiding functions that provide a predictable capacity in the call to make (such as snippet (ii)). Growing dynamically the slice is probably a case we could still leave to the heap.