Go BCE 边界检查消除

837 阅读2分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

Go边界检查消除

什么是边界检查

边界检查是指在使用某一个变量前,检查该变量是否处在一个特定的范围之内。主要为了防止下标索引越界导致内存不安全等问题。在GO中主要是数组、切片下标的检查,如果下标索引越界,会产生Panic。

如何查看是否进行了边界检查

在go 1.7以上的版本,可以使用-gcflags="-d=ssa/check_bce/debug=1"编译器标志来显示哪些代码进行了边界检查

示例

package main
​
func main() {
​
}
​
func f(s []int) {
    _ = s[0] //IsInBounds
    _ = s[1] //IsInBounds
    _ = s[2] //IsInBounds
}
go run -gcflags="-d=ssa/check_bce/debug=1" learn.go
# command-line-arguments
.\learn.go:8:7: Found IsInBounds
.\learn.go:9:7: Found IsInBounds
.\learn.go:10:7: Found IsInBounds

边界检查会带来什么问题

边界检查可以帮助我们早期捕获编程错误,避免更糟糕的情况发生,不可否认,这个是非常必要的。相信很多人都遇到过索引超过数组界限的情况。但进行边界检查肯定就会消耗一部分性能,导致代码运行速度变慢,所以在很多地方,可以通过上下文信息判断,避免很多不必要的边界检查,也就是边界检查消除BCE(Bounds Check Elimination)。

go在1.7版本之前,每处索引都会进行边界检查,1.7版本后采用了新的编译器后端,它基于SSA(静态单赋值形式)。SSA 帮助 Go 编译器有效地使用优化,如BCE(边界检查消除)和CSE(公共子表达式消除),让标准的 Go 编译器可以生成更高效的程序。

虽然编译器已经做了很多优化,通过上下文逻辑判断,去除了很多不必要的边界检查,但很多场景下,编译器不能判断出结果,需要我们明示一下。

BCE优化

例1:

func f(is []int, bs []byte) {
    if len(is) >= 256 {
        for _, n := range bs {
            _ = is[n] // for循环内边界检测会执行多次
        }
    }
}
func f(is []int, bs []byte) {
    if len(is) >= 256 {
        is = is[:256] // 避免下面for循环中的边界检测
        for _, n := range bs {
            _ = is[n] // 边界检测消除!
        }
    }
}

例2

func f(isa []int, isb []int) {
    if len(isa) > 0xFFF {
        for _, n := range isb {
            _ = isa[n & 0xFFF] // for循环内边界检测会执行多次
        }
    }
}
func f(isa []int, isb []int) {
    if len(isa) > 0xFFF {
        isa = isa[:0xFFF+1] // 避免下面for循环中的边界检测
        for _, n := range isb {
            _ = isa[n & 0xFFF] // 边界检测消除!
        }
    }
}

例三

func f(s []int) []int {
    s2 := make([]int, len(s))
    for i := range s {
        s2[i] = -s[i] // 插入边界检测
    }
    return s2
}
func f(s []int) []int {
    s2 := make([]int, len(s))
    s2 = s2[:len(s)] // 避免下面的边界检测
    for i := range s {
        s2[i] = -s[i] // 边界检测消除!
    }
    return s2
}

参考链接

  1. gBounds Checking Elimination
  2. go101.org/article/bou…