go cmd/compile/internal/gc: generalize self-assignment handling in escape analysis

djp7away  于 4个月前  发布在  Go
关注(0)|答案(4)|浏览(121)

目前,至少有两种相当临时的模式被转义分析识别为安全。当它看到不引入需要跟踪的新指针的自赋值语句时,它会跳过详细的分析。最初,只有一些简单的自切片赋值(如上所示)被处理:

x.buf = x.buf[a:b]

后来,添加了一些更多的模式,所以它们也被识别了:

x.ptr1 = x.ptr2
val.pointers[i] = val.pointers[j]

它们的问题在于它们非常脆弱,无法匹配与最简单情况不同的表达式,但仍然表示自赋值:

x.a.b.buf = x.a.b.buf[a:b]

可以通过“基本对象”的概念将所有自赋值模式进行泛化。我们上面看到的是对象字段赋值与对象本身的匹配。基本对象就是那个包含引用字段的对象。对于最简单的情况,基本对象只是“上面”的一个语法级别:

base(x.y)     // => x
base(x.y.z)   // => x.y
base(x[i])    // => x
base(x[i][j]) // => x[i]

对于表达式实际上返回相同对象的情况,我们可以跳过几个级别:

base(x[:])    => x
base(x[:][:]) => x

给定 base 函数,我们可以将自赋值表示为(伪Go):

// See samesafeexpr from cmd/compile/internal/gc/typecheck.go
return samesafeexpr(base(dst), base(src))

这涵盖了上述所有模式,再加上一些更多。作为奖励,它还解决了简单的 *p = *p 情况(参见 #5919 ):

func j(p *string) {
	*p = *p
}

在泛化之前未被识别的更有趣的自赋值示例:

type node struct { next *node }
func createLoop(n *node) {
	n.next = n // n was leaking param before
}
type foo struct {
	pi *int
	i  int
}
func (f *foo) example() {
	f.pi = &f.i // f was leaking param before
}

这个解决方案(如果正确或可以使其正确):

  • 使自赋值检查变得更加临时、强大(希望更有用)
  • 使实现变得更简单,减少代码重复

收集新的转义分析结果信息:

go build -a -gcflags=-m ./src/... 2>&1 | grep 'does not escape' > new_noescapes.txt
go build -a -gcflags=-m ./src/... 2>&1 | grep 'leaking param' > new_leakparam.txt

现在使用未修补的 go tool compile :

go build -a -gcflags=-m ./src/... 2>&1 | grep 'does not escape' > old_noescapes.txt
go build -a -gcflags=-m ./src/... 2>&1 | grep 'leaking param' > old_leakparam.txt

现在可以进行一些比较。
新的非转义值/参数:

src/net/net.go:687:7: (*Buffers).consume v does not escape
src/net/net.go:674:7: (*Buffers).Read v does not escape
src/internal/poll/fd.go:48:14: consume v does not escape
src/compress/flate/inflate.go:325:10: (*decompressor).nextBlock &f.h1 does not escape
src/compress/flate/inflate.go:326:10: (*decompressor).nextBlock &f.h2 does not escape
src/cmd/compile/internal/gc/const.go:668:15: evconst &nl does not escape
src/container/ring/ring.go:121:7: (*Ring).Len r does not escape
src/cmd/compile/internal/types/type.go:727:13: (*Type).copy &nt does not escape

由于 ring.Ring.Len 接收器不再转义,我们可以尝试使用基准测试验证和测量它:

package foo

import (
	"container/ring"
	"testing"
)

func BenchmarkRingLen(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var r ring.Ring
		_ = r.Len()
	}
}

旧的是具有泄漏的 r 的未修补转义分析。新的是非泄漏的 r :

name       old time/op    new time/op    delta
RingLen-8    35.6ns ± 6%     3.0ns ± 0%   -91.46%  (p=0.000 n=10+10)

name       old alloc/op   new alloc/op   delta
RingLen-8     32.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)

name       old allocs/op  new allocs/op  delta
RingLen-8      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)

这里是 ring.Ring.Len 方法实现,供参考:https://golang.org/src/container/ring/ring.go?s=2869:2893#L111
有关其他示例,请参阅建议的测试套件扩展:

package foo

var sink interface{}

func length(xs []byte) int { // ERROR "leaking param: xs"
	sink = xs // ERROR "xs escapes to heap"
	return len(xs)
}

func zero() int { return 0 }

type buffer struct {
	arr        [64]byte
	buf1       []byte
	buf2       []byte
	bufPtr1    *[]byte
	bufPtr2    *[]byte
	bufList    [][]byte
	bufArr     [5][]byte
	bufPtrList []*[]byte
	str1       string
	str2       string
	next       *buffer
	buffers    []*buffer
}

func (b *buffer) getNext() *buffer { // ERROR "leaking param: b to result ~r0 level=1"
	return b.next
}

// When referring to the "old" implementation, cases that are covered in
// escape2.go are implied.
// Most tests here are based on those tests, but with slight changes,
// like extra selector expression level.

func (b *buffer) noEsc() { // ERROR "\(\*buffer\).noEsc b does not escape"
	// Like original slicing self-assignment test, but with additional
	// slicing expressions inside the RHS.
	b.buf1 = b.buf1[:][1:2]        // ERROR "ignoring self-assignment to b.buf1$"
	b.buf1 = b.buf1[1:][1:2:3]     // ERROR "ignoring self-assignment to b.buf1$"
	b.buf1 = b.buf2[:2][1:][1:2]   // ERROR "ignoring self-assignment to b.buf1$"
	b.buf1 = b.buf2[:][1:2][1:2:3] // ERROR "ignoring self-assignment to b.buf1$"

	// The "left" (base) part is generalized and can be arbitrary,
	// as long as it doesn't affect memory.
	// Basically, these cases add "next" in the buf accessing chain.
	b.next.buf1 = b.next.buf1[1:2]             // ERROR "ignoring self-assignment to b.next.buf1$"
	b.next.buf1 = b.next.buf1[1:2:3]           // ERROR "ignoring self-assignment to b.next.buf1$"
	b.next.buf1 = b.next.buf2[1:2]             // ERROR "ignoring self-assignment to b.next.buf1$"
	b.next.next.buf1 = b.next.next.buf2[1:2:3] // ERROR "ignoring self-assignment to b.next.next.buf1$"

	// Indexing functionally is almost identical to field accessing.
	// It's permitted to have different trailing indexes just as it's
	// permitted to have different trailing selectors.
	index1 := 10
	index2 := index1
	b.bufList[0] = b.bufList[1][1:2]             // ERROR "ignoring self-assignment to b.bufList\[0\]$"
	b.bufList[index1] = b.bufList[index2][1:2:3] // ERROR "ignoring self-assignment to b.bufList\[index1]$"
	b.bufArr[0] = b.bufArr[1+1][1:2]             // ERROR "ignoring self-assignment to b.bufArr\[0]$"
	*b.bufPtrList[2] = (*b.bufPtrList[1])[1:2:3] // ERROR "ignoring self-assignment to \*b.bufPtrList\[2\]$"
	// Same indexes should work as well.
	b.bufList[0] = b.bufList[0][1:2]             // ERROR "ignoring self-assignment to b.bufList\[0\]$"
	b.bufList[index1] = b.bufList[index1][1:2:3] // ERROR "ignoring self-assignment to b.bufList\[index1]$"
	b.bufArr[1+1] = b.bufArr[1+1][1:2]           // ERROR "ignoring self-assignment to b.bufArr\[1 \+ 1\]$"
	*b.bufPtrList[2] = (*b.bufPtrList[2])[1:2:3] // ERROR "ignoring self-assignment to \*b.bufPtrList\[2\]$"
	// Works for chained indexing as well, but indexing in base objects must match.
	b.buffers[0].bufList[0] = b.buffers[0].bufList[0][1:2]                           // ERROR "ignoring self-assignment to b.buffers\[0\].bufList\[0\]"
	b.buffers[index1+1].bufList[index1] = b.buffers[index1+1].bufList[index1][1:2:3] // ERROR "ignoring self-assignment to b.buffers\[index1\ \+ 1].bufList\[index1\]"
	b.buffers[1+1].bufArr[1+1] = b.buffers[1+1].bufArr[1+1][1:2]                     // ERROR "ignoring self-assignment to b.buffers\[1 \+ 1\].bufArr\[1 \+ 1\]"
	*b.buffers[1].bufPtrList[2] = (*b.buffers[1].bufPtrList[2])[1:2:3]               // ERROR "ignoring self-assignment to \*b.buffers\[1\].bufPtrList\[2\]"
}

func (b *buffer) esc() { // ERROR "leaking param content: b"
	// None of the cases below should trigger self-assignment optimization.

	// These slice expressions contain sub-exprs that may affect memory.
	b.buf1 = b.buf1[:length(b.buf1)][1:2]
	b.buf1 = b.buf1[1:length(b.buf1)][1:2:3]
	b.buf1 = b.buf2[:][zero():length(b.buf2)][1:2]
	b.buf1 = b.buf2[zero()+1:][:][1:2:3]

	// Due to method call inside the chain, these should not be optimized.
	// The baseObject(x) returns b.getNext() node for both sides,
	// but samesafeexpr would not consider them as "same".
	b.getNext().buf1 = b.getNext().buf1[1:2]
	b.getNext().buf1 = b.getNext().buf1[1:2:3]
	b.getNext().buf1 = b.getNext().buf2[1:2]
	b.getNext().buf1 = b.getNext().buf2[1:2:3]

	// Different base objects.
	b.next.next.buf1 = b.next.buf1[1:2]
	b.next.next.buf1 = b.next.buf1[1:2:3]
	b.next.buf1 = b.next.next.buf2[1:2]
	b.next.buf1 = b.next.next.buf2[1:2:3]
	b.bufList[0] = b.bufArr[0][1:2]

	// Different indexes are not permitted inside base objects.
	index1 := 10
	index2 := index1
	b.buffers[0].bufList[0] = b.buffers[1].bufList[0][1:2]
	b.buffers[index1].bufList[index1] = b.buffers[index2].bufList[index1][1:2:3]
	b.buffers[1+1].bufArr[1+1] = b.buffers[1+0].bufArr[1+1][1:2]
	*b.buffers[0].bufPtrList[2] = (*b.buffers[1].bufPtrList[2])[1:2:3]
	b.buffers[index1+1].bufList[index1] = b.buffers[index1+2].bufList[index1][1:2:3]
}

func (b *buffer) sanity1() { // ERROR "leaking param content: b"
	b.next.buf1 = b.next.buf2[:] // ERROR "ignoring self-assignment to b.next.buf1"
	sink = b.next.buf1           // ERROR "b.next.buf1 escapes to heap"
	sink = b.next.buf2           // ERROR "b.next.buf2 escapes to heap"
}

func (b *buffer) sanity2() { // ERROR "b does not escape"
	b.bufList = b.bufList[:len(b.bufList)-1] // ERROR "ignoring self-assignment to b.bufList"
}
a7qyws3x

a7qyws3x1#

https://golang.org/cl/136496提到了这个问题:cmd/compile/internal/gc: generalize self-assignment

oiopk7p5

oiopk7p52#

/cc @randall77@dr2chase

wixjitnu

wixjitnu3#

我稍微担心x是一个切片或者它的元素是切片的情况。
我认为切片解引用计数被视为间接访问,而数组解引用则不是。
在esc.go中搜索IsArray,我认为你会看到这种区别。
向前迈出最安全的一步可能是在所有索引情况上都加上,看看只使用点号得到的结果,然后在一个单独的CL中将数组(而不是切片!)添加到基本测试中,以防有细微的错误。
ODOT是不同的,因为我认为在那里我们会在有显式解引用时重写为ODOTPTR。

vwhgwdsa

vwhgwdsa4#

https://golang.org/cl/172421提到了这个问题:test: add regress test cases for self-assignment

相关问题