上下文
当使用可变容量分配切片(例如 make([]int, 0, n))时,编译器会得出结论认为它逃逸到了堆上,尽管切片实际上并没有逃逸。
示例:
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)
}
请注意,这与关于类似结构体的逃逸结论形成了对比:
- 代码片段(i):没有逃逸,但由于需要增长而在堆上分配
- 代码片段(ii):逃逸(即使实际上并没有),因为在构建时无法确定分配容量
- 代码片段(iii):没有逃逸,按预期在栈上分配
建议
我建议在可能的情况下更积极地在栈上分配切片,无论在构建时是否知道容量。
代码片段(ii)应该被检测为没有逃逸(我相信在某个时候是这样的,我们因为可变容量而恢复到逃逸)。这将使逃逸分析与Map保持一致。
将非逃逸切片分配到栈或堆的决定应推迟到运行时,优先考虑栈,仅在较大的切片中才转到堆。
至少,这应该有利于遵守规则的函数,它们在调用 make 时提供了可预测的容量(如代码片段(ii))。动态增长切片可能是我们仍然可以留给堆的情况。
5条答案
按热度按时间sdnqo3pr1#
关于Map的相关内容提案:#58214
djmepvbi2#
当前,我们的所有堆栈帧都是固定大小的,因此在堆栈上分配动态大小的任何内容都是一个庞大的项目。
您可以像这样模拟所需的操作:
这将在n <= 64时进行堆栈分配,否则进行堆分配。
我想我们可以自动完成这种转换,但我们需要知道正确的数字
64
,我认为这取决于应用程序。oalqel3c3#
将提案过程剔除,因为这只是一个优化。没有新的API。
plicqrtu4#
在@randall77的评论基础上,我们曾简要考虑过一种非堆栈但与堆栈帧绑定的动态大小分配器(以及出于一些更“简单”的原因而逃脱的其他事物),但当应用程序正在使用解决方法时(如@randall77所指出的),总收益尚不清楚。
w80xi6nr5#
感谢@randall77提供的提示。我会尝试一下。这确实是一种hack...