给定以下代码:
/* 1*/ package example
/* 2*/
/* 3*/ var sink interface{}
/* 4*/
/* 5*/ func fn() {
/* 6*/ var x *int
/* 7*/ x = new(int)
/* 8*/ sink = x // Sink 1 at the line 8; first new(int) flows here
/* 9*/ x = new(int)
/*10*/ sink = x // Sink 2 at the line 10; second new(int) flows here
/*11*/ }
(带有行号的代码已注解,问题描述后概述了最小示例。)
执行命令 $ go tool compile -m=2 example.go
。
提示上的输出是(重要的位用粗体表示):
example.go:5:6: can inline fn as: func() { var x *int; x = ; x = new(int); sink = x; x = new(int); sink = x }
example.go:8:7: x escapes to heap
example.go:8:7: from sink (assigned to top level variable) at example.go:8:7
example.go:7:9: new(int) escapes to heap
example.go:7:9: from x (assigned) at example.go:7:4
**example.go:7:9: from x (interface-converted) at example.go:8:7
example.go:7:9: from sink (assigned to top level variable) at example.go:8:7**
example.go:9:9: new(int) escapes to heap
example.go:9:9: from x (assigned) at example.go:9:4
**example.go:9:9: from x (interface-converted) at example.go:8:7
example.go:9:9: from sink (assigned to top level variable) at example.go:8:7**
example.go:10:7: x escapes to heap
example.go:10:7: from sink (assigned to top level variable) at example.go:10:7
预期输出不会报告第二个汇点位于相同的位置。
这是图形构建和打印的结果。
简化示例:
package example
var sink *int
func fn() {
var x *int
x = new(int)
sink = x
x = new(int)
sink = x
}
输出:
example.go:5:6: can inline fn as: func() { var x *int; x = ; x = new(int); sink = x; x = new(int); sink = x }
example.go:7:9: new(int) escapes to heap
**example.go:7:9: from x (assigned) at example.go:7:4
example.go:7:9: from sink (assigned to top level variable) at example.go:8:7**
example.go:9:9: new(int) escapes to heap
**example.go:9:9: from x (assigned) at example.go:9:4
example.go:9:9: from sink (assigned to top level variable) at example.go:8:7**
预期输出:
example.go:5:6: can inline fn as: func() { var x *int; x = ; x = new(int); sink = x; x = new(int); sink = x }
example.go:7:9: new(int) escapes to heap
**example.go:7:9: from x (assigned) at example.go:7:4
example.go:7:9: from sink (assigned to top level variable) at example.go:8:7**
example.go:9:9: new(int) escapes to heap
**example.go:9:9: from x (assigned) at example.go:9:4
example.go:9:9: from sink (assigned to top level variable) at example.go:10:7**
对于这个例子,我们有类似这样的东西:
sink
伪节点有两个源边,都来自x
节点。x
节点有两个源边,它们来自两个不同的new(int)
sink.Flowsrc == { {dst=sink, src=x, where=line8}, {dst=sink, src=x, where=line10} }
x.Flowsrc == { {dst=x, src=new(int), where=line7 }, {dst=x, src=new(int), where=line9} }
在 escwalkBody
中的遍历过程中,使用第一个 sink
目标作为父节点打印了两个 new(int)
路径,所以我们得到了两个相同的路径。由于 osrcesc
变量检查用于避免重复消息,因此没有打印第二个目的地。
如果删除 osrcesc
,两个路径都会打印两次(因此,总共有4条消息而不是2条,但其中2条是正确的)。目前, osrcesc
导致了2条消息,其中一条是错误的。
仅仅检查目标节点是否位于实际起始流点之前是不够的,因为存在递归函数:
func f8(x int, y *int) *int {
if x <= 0 {
return y
}
x--
return f8(*y, &x)
}
在这里, return y
是目标端点,它出现在跟踪的 &x
之前。
我对此没有好的想法来解决这个问题。
希望上面的见解能帮助某人解决这个问题。
4条答案
按热度按时间zpjtge221#
不知道该抄送给谁。也许可以@josharian?(这只是为了验证问题;也许这是esc.go解释器已知并接受的限制。)
hgqdbh6s2#
@dr2chase是主要的逃逸分析Maven(据我所知);他编写了逃逸分析解释器。另一个想到的人是@cherrymui。
wlsrxk513#
在我看来,一个更深层次的问题是数据流分析是在节点级别进行的,其中所有
x
的出现都用相同的节点表示。例如,如果我删除第二个sink=x
赋值,即package example
var sink *int
func fn() {
var x *int
x = new(int)
sink = x
x = new(int)
_ = x
}
第二个
new(int)
不一定会逃逸。在当前的分析中,它确实会逃逸,因为new(int)
流向x
和x
,在某个时刻流向堆。在原始程序中,实际上并不关心x
在何时/何地流向堆;它只是选择一个。为此,我认为分析需要在一个跟踪数据流顺序的层级上工作(可能是某种形式的 SSA)。自然而然地,更准确的位置报告就会跟上。
shyt4zoc4#
第二个新的(int)不一定需要转义。根据当前的分析,它确实需要转义,因为新的(int)流向x,而x在某个时刻流向堆。在原始程序中,实际上并不关心x何时/何地流向堆;它只是选择一个。
我实际上尝试过制作完全相同的内容,以便让逃逸分析识别出第二个
new(int)
是不必要的,但遇到了意外的调试输出,这让我感到疑惑。