Golang Bounds Check 和 Bounds Check Elimination

808 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

程序中的越界panic

Golang里面,异常叫做panic,我们经常遇到的一种panic是越界。

比如说,你声明了一个数组,长度是4,你非要访问下标是8的元素:

var a [4]int
x := 8
fmt.Println(a[x])

运行就会报下面的错:

panic: runtime error: index out of range [8] with length 4

翻译过来就是,runtime 错误,下标[8]超范围了,长度只有4。

这种panic是runtime行为

所谓runtime行为,就是程序跑起来之后的行为,也就是说,对数组进行下标操作的时候,也就是去取一个下标的值的时候,Go 会帮你检测一下,是否越界了,如果越界了,就会panic。

既然是上面的逻辑,那么这种逻辑是帮了我们犯更大的错误,因为越界本身,可能让你取了一个完全是未知内存里的数据,导致你的业务逻辑凉凉,比如说这一块的逻辑是跟金额有关的话。

这种行为就成为 Bounds Check,翻译就是,边界检测。

Bounds Check 的坏处

说坏处的话,那么就是慢了。尤其是你如果有大量的对于数组啦,slice啦,啥的,这种取值,重新slice这种操作的时候。

比如说,图像处理啥的, 全是这种数组或者slice。如果每个操作都要进行 check,真的会很慢。

那么有没有办法能够尽量减少这种 check 呢?

Bounds Check Elimination

翻译过来,就是边界检测消除。

看一个例子就能明白:

var a [4]int
fmt.Println(a[1])

a是一个数组,长度是4。

我们下面打印了a中下标1的元素,此时此刻,完全不用runtime进行检测,为啥。这不可能越界。

这种编译时期就一定会知道不会越界的,那么runtime就应该不检测。

基于这种想法,Go 团队搞了一大堆,不需要check的场景出来。

我们来看几个:

func fn1(s []int){
    _ = s[2] // 第一句
    _ = s[1] // 第二句
    _ = s[0] // 第三句
}

请问,上面三句,哪些需要检测,哪些不用检测。

答案: 第一句需要检测,第二句和第三句都可以不用检测。这很容易理解,都能访问下标2了,那么肯定能访问下标1和下标0。

再来看:

func fn2(s []int){
    if len(s) > 2 {
        _, _, _ = s[0], s[1], s[2]
    }
}

由于if判断了s的长度肯定是超过2的,所以下面这一行,三个下标操作,全部都可以不用检测了。

再来看一个必须检测的:

func fn3(s []int, i int){
    _ = s[:i] // 第一句
    _ = s[i:] // 第二句
}

第一句,没啥可说的,肯定要进行检测。

那么第二句,是否可以消掉,不检测了呢?

答案是不行。

先看第一句,第一句中的i只要满足,i<=cap(s)。
假设i==cap(s)i==cap(s),很明显,第二句肯定会出错,所以第二句还是要进行正常的检测,然后panic出来。

使用编译参数获取详细信息

如果你想知道,你代码里,哪一行进行了这种检测,你只需要在

go build 或者 go run 后面带上:

-gcflags="-d=ssa/check_bce/debug=1"

看下面的例子:

package main

import (
	"fmt"
	"math/rand"
)

func asd() {
	a := make([]int, 3, 10)
	s := rand.Int()
	b := a[s:]
	fmt.Println(b)

}
func main() {
}

存到main.go里,执行:

go run -gcflags="-d=ssa/check_bce/debug=1" main.go

就会输出:

# command-line-arguments
./main.go:11:8: Found IsSliceInBounds

就是说,11行,要进行检测。