Go1.7中的邊界檢查消除

Go1.7中的邊界檢查消除

原文連接:http://www.tapirgames.com/blog/golang-1.7-bcegolang

最近發佈的 Go 1.7 使用了一個新的基於靜態單賦值(SSA)的編譯器後端(不只在amd64中可用)。SSA使得go編譯器生成更高效的優化代碼,好比 邊界檢查消除(BCE)常見的字表達式消除。 這篇文章將展現一些例子以說明BCE在官方的Go編譯器1.7+中是怎樣工做的。 在Go 1.7+,你能夠運行 go build -gcflags="-d=ssa/check_bce/debug=1" 來展現哪些代碼行須要檢查邊界。express

Example 1

// example1.go
package main

func 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 中的 line 12 和 line 13 不須要檢查邊界了,在 line 11 的邊界檢查保證了 line 12 和 line 13 的索引不會越界。 可是在函數 f1 中,所有的3行代碼都執行了邊界檢查。line 5 的邊界檢查不能保證 line 6 和 line 7 是安全的,line 6 的邊界檢查也不能保證 line 7 是安全的。 對於函數 f3 來講,若是第一個 s[index] 是安全的,Go 1.7+ 編譯器就能知道第二個 s[index] 絕對是安全的。 Go 1.7+ 編譯器也能準確的知道函數 f4 中惟一的一行(line 22) 也是安全的。後端

Example 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

能夠看到在 example2.go 中只有一行代碼須要邊界檢查。 Go 1.7+ 編譯器能夠聰明地作出結論:在函數 f5, f6, f8 和 f9 中的全部行都是安全的。 啊哈,看起來它尚未像人類那樣聰明,line 22 看起來也是安全的。api

Example 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?
}

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, not smart enough?
}

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

Oh,還有這麼多地方須要邊界檢查! 可是等等,爲何 Go 1.7+ 編譯器認爲 line 10 是安全的可是 line 15 和 line 23 卻不是呢?是編譯器不夠聰明仍是有bug? 事實上,編譯器是正確的!爲何?緣由是子切片的長度可能比原來的切片長。例如:安全

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]是安全的那麼s[index:]也是安全的 只有在 len(s)==cap(s) 的狀況才成立。這就是爲何 example 3 中函數 fb 和 fc 的代碼行仍須要作邊界檢查。 Go 1.7+ 編譯器成功地在函數 fa 中檢查到 len(s) == cap(s) 。作得好,Golang team! 然而,若是s[index:]是安全的那麼s[:index]也是安全的 這句話看起來老是成立的。(譯者注:注意index的位置,和前面是相反的)bash

Example 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(和1.8)成功地判斷出函數 fc2 中當 line 22 是安全的那麼 line 23 也是安全的,可是在函數 fb2 中當 line 14 是安全的時沒有判斷出 line 15 也是安全的。app

Example 5

雖然當前的編譯器(Go 1.7.1 amd64 和 Go 1.8)沒有足夠聰明到消除一些沒必要要的邊界檢查,可是咱們能夠給編譯器一些提示來幫助編譯器消除這些沒必要要的邊界檢查。ide

// 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

總結

雖然官方的 Go 1.7(和1.8)編譯器的 BCE 特性還不是很完美,但在不少常見的場景中它已經作得很好。毫無疑問地,Go 編譯器在以後的版本會作得更好。感謝 Go 團隊添加了這個漂亮的特性!函數

引用

  1. Bounds Checking Elimination - Google 文檔
  2. Utilizing the Go 1.7 SSA Compiler
相關文章
相關標籤/搜索