Go1.7裡面的BCE(跳躍檢測排除)(譯)

astaxie發表於2016-11-10

最近釋出的 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.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 函式裡面的 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 團隊增加了如此幫的特性

參考文獻

  1. gBounds Checking Elimination
  2. Utilizing the Go 1.7 SSA Compiler

原文:http://www.tapirgames.com/blog/golang-1.7-bce

更多原創文章乾貨分享,請關注公眾號
  • Go1.7裡面的BCE(跳躍檢測排除)(譯)
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章