在Golang逃逸分析中,“leak/leaking param”的含义是什么?

9
func main() {
        i1 := 1
    A1(&i1)
}

func A1(i1 *int) *int {
    return i1
}

逃逸分析的结果是:

./main.go:18:9: parameter i1 leaks to \~r1 with derefs=0:
./main.go:18:9:   flow: \~r1 = i1:
./main.go:18:9:     from return i1 (return) at ./main.go:19:2
./main.go:18:9: leaking param: i1 to result \~r1 level=0

“parameter i1 leaks to \~r1 with derefs=0” 的意思是参数 i1 泄漏到了内存区域 \~r1 ,且没有被引用(derefs=0)。

“leaking param: i1 to result \~r1 level=0” 的意思是参数 i1 漏出到结果返回值 \~r1 中,并且层级为 0。

首先,我尝试谷歌搜索“golang escape leaking”,最相关的结果出现在escape-analysis-shows-channel-as-leaking-param的评论中。

“你为什么会这样认为?”假设泄露是不好的,并与其词源 leak 相关。 我正在努力想出泄漏是好事的例子,例如泄漏的桶,泄漏的气罐,排放物,泄漏的电容器,漏水的船,泄漏的抽象。 对于高性能的 Go 专家来说可能显而易见,但对于我们其他人来说,将文档链接和提供有关 leaking param 是什么的简要澄清将会很有帮助。

这正是我想要问的问题,但之后就没有更多的回复了。

接着,我试图阅读打印这些结果的源代码。

compile/internal/escape/leaks.go中,我发现了以下注释:

// An leaks represents a set of assignment flows from a parameter

// to the heap or to any of its function's (first numEscResults)

// result parameters.

但是我不能理解这个注释,是否有官方文档来解释呢?

此外,在源代码中我还发现了一个问题:当numEscResults(7)之后的结果参数会在运行时逃逸到堆中吗?

func main() {
    i1, i2, i3, i4, i5, i6, i7, i8, i9 := 1, 1, 1, 1, 1, 1, 1, 1, 1
    A1(&i1, &i2, &i3, &i4, &i5, &i6, &i7, &i8, &i9)
    return
}

func A1(i1, i2, i3, i4, i5, i6, i7, i8, i9 *int) (*int, *int, *int, *int, *int, *int, *int, *int, *int) {
    return i1, i2, i3, i4, i5, i6, i7, i8, i9
}

...some duplicate output
./main.go:16:13: leaking param: i2 to result ~r10 level=0
./main.go:16:17: leaking param: i3 to result ~r11 level=0
./main.go:16:21: leaking param: i4 to result ~r12 level=0
./main.go:16:25: leaking param: i5 to result ~r13 level=0
./main.go:16:29: leaking param: i6 to result ~r14 level=0
./main.go:16:33: leaking param: i7 to result ~r15 level=0
./main.go:16:37: leaking param: i8
./main.go:16:41: leaking param: i9
./main.go:8:30: i8 escapes to heap:
./main.go:8:30:   flow: {heap} = &i8:
./main.go:8:30:     from &i8 (address-of) at ./main.go:9:40
./main.go:8:30:     from A1(&i1, &i2, &i3, &i4, &i5, &i6, &i7, &i8, &i9) (call parameter) at ./main.go:9:4
./main.go:8:34: i9 escapes to heap:
./main.go:8:34:   flow: {heap} = &i9:
./main.go:8:34:     from &i9 (address-of) at ./main.go:9:45
./main.go:8:34:     from A1(&i1, &i2, &i3, &i4, &i5, &i6, &i7, &i8, &i9) (call parameter) at ./main.go:9:4
./main.go:8:30: moved to heap: i8
./main.go:8:34: moved to heap: i9

3
区分堆和栈的分配是为了提高性能。这是编译器内部的细节,写正确和快速的 Go 程序不需要理解这个。决定堆和栈分配的编译器内部机制必须证明可以进行栈分配。如果变量可能“泄漏”,则无法进行栈分配。忽略这些,编译器会做正确的工作。如果你不是负责编译器优化的编译器编写人员,那么这里真的没有太多值得关注的东西。 - Volker
1
谢谢您的评论。我并没有参与编译器的任何部分,我相信Go编译器总是能够做出正确的工作。我只是出于好奇想要理解它。在我能够理解它之前,是否需要掌握大量编译器原理等知识?请问有哪些关键词可以提供一些线索呢? - MungBeanSoup
阅读有关堆栈和堆的区别的内容,例如在C语言中。Go将东西放在堆上,除非它可以证明可以放在堆栈上。这种证明被称为“逃逸分析”。由于这是一个广泛的领域,而且在几乎所有情况下都不是必要的或有帮助的,因此您需要向人类提问。我建议不要烦恼这种东西。这就像在完全不了解量子力学的情况下询问量子算法中的“关键字线索”一样。 - Volker
1个回答

11
参数i1泄漏到~r1,解引用=0并泄漏参数:i1到结果~r1级别=0的含义是什么?
TLDL: 如果你在查找分配,请忽略泄漏的参数并寻找“移动到堆栈”的部分。
“泄漏的参数”意味着该函数在返回后以某种方式保持其参数活动状态,这并不意味着它被移动到堆栈,事实上大多数“泄漏的参数”都是在堆栈上分配的。
“r1”指的是函数的返回值,它从0开始,因此“r1”表示第二个返回值。(与提供的示例代码不匹配,应该是r0)。对于第一个片段,它泄漏了“i1”,因为“r0 = i1”,所以第0个函数的返回值是“i1”,因此“i1”在返回后必须保持活动状态,并向调用者“泄漏”。
编译器输出中出现在“泄漏的参数”之前的内容是因为OP使用了“-m -m”,其打印数据流图。
对于dereference,来自cmd/compile/internal/escape/escape.go中的注释:
"[...] 记录解引用操作数减去寻址操作数的数量作为边的权重(称为“derefs”)。”
“level”在当前注释中没有描述,而且我已经很久没有熟悉gc源代码了,据我所知,这是内存间接级别,一次间接(*)操作会增加一次,取地址(&)会减少一次,因此该函数。
func A1(a **int) *int {
    p := &a
    return **p
}

应该提供一个泄漏参数a,级别为1。

此外,在源代码中我还发现了一个问题。如果numEscResults(7)之后的结果参数在运行时逃逸到堆上会怎样?

是的,所有依赖于泄漏参数的第7个及其后面的返回结果都将移动到堆上。我不确定为什么是7,但是根据我的经验和gc源代码,我猜测这个值不会太大程度地减慢编译速度,同时也可以保留大多数函数的优化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接