go 运行时:允许将未知容量的不带转义的切片分配给栈,但分配方式更加激进,

wd2eg0qa  于 6个月前  发布在  Go
关注(0)|答案(5)|浏览(45)

上下文

当使用可变容量分配切片(例如 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))。动态增长切片可能是我们仍然可以留给堆的情况。

sdnqo3pr

sdnqo3pr1#

关于Map的相关内容提案:#58214

djmepvbi

djmepvbi2#

当前,我们的所有堆栈帧都是固定大小的,因此在堆栈上分配动态大小的任何内容都是一个庞大的项目。
您可以像这样模拟所需的操作:

index := append(make([]int, 0, 64), make([]int, n)...)

这将在n <= 64时进行堆栈分配,否则进行堆分配。
我想我们可以自动完成这种转换,但我们需要知道正确的数字64,我认为这取决于应用程序。

oalqel3c

oalqel3c3#

将提案过程剔除,因为这只是一个优化。没有新的API。

plicqrtu

plicqrtu4#

在@randall77的评论基础上,我们曾简要考虑过一种非堆栈但与堆栈帧绑定的动态大小分配器(以及出于一些更“简单”的原因而逃脱的其他事物),但当应用程序正在使用解决方法时(如@randall77所指出的),总收益尚不清楚。

w80xi6nr

w80xi6nr5#

感谢@randall77提供的提示。我会尝试一下。这确实是一种hack...

相关问题