-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
Context
Whenever a map is created and identified as not escaping the called function,
I observe that the first bucket (8 entries) comes literally for free on the stack.
However, when the map is grown to a higher number of buckets (e.g. make(map[int]string,9) or grown dynamically),
the next buckets are allocated on the heap, then garbage-collected.
This makes a huge difference for relatively small maps, but not as small as the tiny 8 slots.
The same occurs, whether the map size is known or not known at compile time, yet correctly detected as not escaping the function.
Proposal
I suggest allocating buckets of unescaped maps to the stack more aggressively and only resorting to the heap
when the stack is getting close to its max.
The criterion to switch from stack to heap should be determined at runtime and could be either when the stack is maxed out (super aggressive) or merely when allocating the new bucket would require a new stack segment (super conservative).
In either case, I believe this improvement would largely speed up the dozens of small map-based utility functions that we use routinely (see an example below).
Example
In the 3 simplistic examples below (all inspired by real-life utilities, such as "deduplicate an input slice", but much simplified here...), the compiler correctly identifies the map as not escaping the func.
In all examples, if the input size (variable or const) is <= 8, no allocation on the heap takes place (the first bucket comes for free).
In all examples, if an extra bucket is needed, either by calling make with a hint or by growing the map, it is always allocated on the heap.
const ok = "ok"
// dynamic allocation, up to a capacity not known at build time
func mapAllocString(n int) int {
index := make(map[int]string) // does not escape: ok
for i := 0; i < n; i++ { // if n>8, dynamic allocation of new buckets occurs on the heap
index[i] = ok
}
return len(index)
}
// pre-allocation with hint, up to a capacity not known at build time
func mapAllocMakeString(n int) int {
index := make(map[int]string, n) // does not escape: ok if n>8, pre-allocation of new buckets occurs on the heap
for i := 0; i < n; i++ {
index[i] = ok
}
return len(index)
}
// pre-allocation with hint, up to a capacity known at build time
func mapAllocMakeConstString(_ int) int {
const size = 32
index := make(map[int]string, 32) // does not escape: ok if size>8, pre-allocation of new buckets occurs on the heap
for i := 0; i < size; i++ {
index[i] = ok
}
return len(index)
}