最近发布的 Go 1.7 里面使用了一个新的基于 SSA 的编译后端(目前只有 amd64 可用)。SSA 使得编译出来的代码更加高效 SSA,主要是里面有包含了 BCE和公共子表达式消除。
这篇文章将会通过一些例子给大家展示 Go 1.7 里面BCE是如何工作的。
在 Go 1.7 里面我们可以通过这个命令 go build -gcflags="-d=ssa/check_bce/debug=1" 来展示我们的代码哪一行需要进行越界检查。
例子1
// example1.gopackage mainfunc f1(s []int) {
_ = s[0] // line 5: bounds check
_ = s[1] // line 6: bounds check
_ = s[2] // line 7: bounds check }func f2(s []int) {
_ = s[2] // line 11: bounds check
_ = s[1] // line 12: bounds check eliminatd!
_ = s[0] // line 13: bounds check eliminatd!}func f3(s []int, index int) {
_ = s[index] // line 17: bounds check
_ = s[index] // line 18: bounds check eliminatd!}func f4(a [5]int) {
_ = a[4] // line 22: bounds check eliminatd!}func main() {}
$ go build -gcflags="-d=ssa/check_bce/debug=1" example1.go
# command-line-arguments
./11.go:5: Found IsInBounds
./11.go:6: Found IsInBounds
./11.go:7: Found IsInBounds
./11.go:11: Found IsInBounds
./11.go:17: Found IsInBounds
我们可以看到这个f2函数里面的12行和13行不需要进行越界检查,因为在11行里面的越界检查可以保证12,13行的代码是不会越界的。
但是在f1里面必须每一行都逐一检查越界,因为第5行没办法保证第6、第7行是安全的,第6行没办法保证第7行是安全的。
函数f3里面,Go 1.7 编译器知道如果第一行是安全的情况下,那么第二行的s[index]是完全安全的
Go 1.7 的编辑器也正确的分析出来了f4函数里面的唯一的一行(22行)也是安全的。
例子2
// example2.go
package main
func f5(s []int) {
for i := range s {
_ = s[i]
_ = s[i:len(s)]
_ = s[:i+1]
}
}
func f6(s []int) {
for i := 0; i < len(s); i ++ {
_ = s[i]
_ = s[i:len(s)]
_ = s[:i+1]
}
}
func f7(s []int) {
for i := len(s) - 1; i >= 0; i -- {
_ = s[i] // line 22: bounds check
_ = s[i:len(s)]
}
}
func f8(s []int, index int) {
if index >= 0 && index < len(s) {
_ = s[index]
_ = s[index:len(s)]
}
}
func f9(s []int) {
if len(s) > 2 {
_, _, _ = s[0], s[1], s[2]
}
}
func main() {}
$ go build -gcflags="-d=ssa/check_bce/debug=1" example2.go
# command-line-arguments
./11.go:22: Found IsInBounds
我们可以看到在例子2里面只有一行需要进行越界检查
Go 1.7 编译器是如此的聪明,它精确的做出决定说:在f5和f6里面所有的代码都是安全的
但是好像还是没有我们人类聪明,看上去22行也是安全的
例子3
// example3.go
package main
import "math/rand"
func fa() {
s := []int{0, 1, 2, 3, 4, 5, 6}
index := rand.Intn(7)
_ = s[:index] // line 9: bounds check
_ = s[index:] // line 10: bounds check eliminatd!
}
func fb(s []int, index int) {
_ = s[:index] // line 14: bounds check
_ = s[index:] // line 15: bounds check // not smart enough or a bug?
}
func fc() {
s := []int{0, 1, 2, 3, 4, 5, 6}
s = s[:4]
index := rand.Intn(7)
_ = s[:index] // line 22: bounds check
_ = s[index:] // line 23: bounds check
}
func main() {}
$ go build -gcflags="-d=ssa/check_bce/debug=1" example3.go
# command-line-arguments
./11.go:9: Found IsSliceInBounds
./11.go:14: Found IsSliceInBounds
./11.go:15: Found IsSliceInBounds
./11.go:22: Found IsSliceInBounds
./11.go:23: Found IsSliceInBounds
啊!那么多地方需要进行越界检查
但是等等,为什么 Go 1.7 的编译器会认为第10行是安全的,但是15行和23行不安全?是因为编译器不够聪明还是这是一个bug?
实际上,编译器在这里是对的!为什么?原因是子slice可能大于原始的slice,看下面这个例子:
package main
func main() {
s0 := make([]int, 5, 10) // len(s0) == 5, cap(s0) == 10
index := 8
// In golang, for the subslice syntax s[a:b],
// the valid rage for a is [0, len(s)],
// the valid rage for b is [a, cap(s)].
// So, this line is no problem.
_ = s0[:index]
// But, above line is safe can't assure the following line is also safe.
// In fact, it will panic.
_ = s0[index:] // panic: runtime error: slice bounds out of range
}
所以如果s[:index]是安全的话,也就当len(s) == cap(s)的时候,s[index:]才是安全的
。这就是为什么在例子3里面fb和fc里面的代码还需要进行越界检查。
Go 1.7 编译器在函数fa里面成功的检测到了 len(s) == cap(s)。太棒了!Golang Team!
然而,如果s[index:]是安全的话,那么s[:index]就一直是安全的。
例子4
// example4.go
package main
import "math/rand"
func fa2() {
s := []int{0, 1, 2, 3, 4, 5, 6}
index := rand.Intn(7)
_ = s[index:] // line 9: bounds check
_ = s[:index] // line 10: bounds check eliminatd!
}
func fb2(s []int, index int) {
_ = s[index:] // line 14: bounds check
_ = s[:index] // line 15: bounds check // not smart enough?
}
func fc2() {
s := []int{0, 1, 2, 3, 4, 5, 6}
s = s[:4]
index := rand.Intn(7)
_ = s[index:] // line 22: bounds check
_ = s[:index] // line 23: bounds check eliminatd!
}
func main() {}
$ go build -gcflags="-d=ssa/check_bce/debug=1" example4.go
# command-line-arguments
./11.go:9: Found IsSliceInBounds
./11.go:14: Found IsSliceInBounds
./11.go:15: Found IsSliceInBounds
./11.go:22: Found IsSliceInBounds
在这个例子中, Go 1.7 编译器成功的计算出来在fc2函数里面如果22行安全的情况下那么23行也是安全的,但是在fb2里面没办法计算出来如果14行安全的情况下15行也是安全的情况。
例子5
尽管当前的编译器(Go1.7.1 amd64) 还是不够智能的排除一些不需要检测的地方,但是我们可以做一些暗示来帮助编译器排除这些不必要的越界检查。
// example5.go
package main
func fd(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // line 7: bounds check, not smart enough.
}
}
}
func fd2(is []int, bs []byte) {
if len(is) >= 256 {
is = is[:256] // line 14: bounds check. A hint for the compiler.
for _, n := range bs {
_ = is[n] // line 16: bounds check eliminatd!
}
}
}
func fe(isa []int, isb []int) {
if len(isa) > 0xFFF {
for _, n := range isb {
_ = isa[n & 0xFFF] // line 24: bounds check, not smart enough.
}
}
}
func fe2(isa []int, isb []int) {
if len(isa) > 0xFFF {
isa = isa[:0xFFF+1] // line 31: bounds check. A hint for the compiler.
for _, n := range isb {
_ = isa[n & 0xFFF] // line 33: bounds check eliminatd!
}
}
}
func ff(s []int) []int {
s2 := make([]int, len(s))
for i := range s {
s2[i] = -s[i] // line 41: bounds check, not smart enough.
}
return s2
}
func ff2(s []int) []int {
s2 := make([]int, len(s))
s2 = s2[:len(s)] // line 48: bounds check. A hint for the compiler.
for i := range s {
s2[i] = -s[i] // line 50: bounds check eliminatd!
}
return s2
}
func main() {}
$ go build -gcflags="-d=ssa/check_bce/debug=1" example5.go
# command-line-arguments
./11.go:7: Found IsInBounds
./11.go:14: Found IsSliceInBounds
./11.go:24: Found IsInBounds
./11.go:31: Found IsSliceInBounds
./11.go:41: Found IsInBounds
./11.go:48: Found IsSliceInBounds
总结
尽管当前1.7版本里面的 BCE 特性还不够完美,但是在大多数情况下还是表现的非常好。毫无疑问Go的编译器将会在后续版本里面做的更好,感谢 Go 团队增加了如此帮的特性
参考文献
gBounds Checking Elimination
Utilizing the Go 1.7 SSA Compiler
原文:http://www.tapirgames.com/blog/golang-1.7-bce
有疑问加站长微信联系(非本文作者)