概述
自Go 1.7以后,标准Go编译器采用了一个新的编译器后端。该后端基于静态单赋值形式(简称SSA)。SSA利用BCE(Bounds Check Elimination)和CSE(Common Subexpression Elimination)优化可以让Go编译器生成更高效的代码。
BCE顾名思义,叫做边界检测消除。通常,Go程序中对一个slice或string的越界访问会抛出异常。有两种形式的边界检测:索引(a[i])和切片(a[i:j])。Go编译器会在每个访问处插入边界检测的代码。不可否认,边界检测是非常重要的。它能帮助我们在早期捕获常见的编程错误。但是在大多数情况下,这都是没有必要的,完全可以根据上下文进行推断。例:
1
2
3
4
5
6
7
8
9
|
package main
func f(s []int) {
_ = s[2]
_ = s[1]
_ = s[0]
}
func main() {}
|
在Go的早期版本(1.7之前),上面的方法中每处切片索引都会做边界检测。但是我们知道,只需要给_ = s[2]
添加边界检测。因为如果索引2未越界,1和0不可能越界。
在Go 1.7之后的版本中,后两行代码的边界检测就被消除了。可以使用-gcflags="-d=ssa/check_bce/debug=1"
查看是否引入了边界检测:
1
|
$ go build -gcflags="-d=ssa/check_bce/debug=1" main.go
|
输出:
1
|
.\main.go:4:7: Found IsInBounds
|
那么什么情况下的边界检测会消除呢?
边界检测消除的情形
1. 重复检测
相同的索引被重复访问时,后面的访问无需边界检测。例:
1
2
3
4
|
var s[]int
_ = s[i] // 插入边界检测
_ = s[i] // 相同索引,不需要
|
1
2
|
_ = s[:i] // 插入边界检测
_ = s[:i] // 不需要
|
1
2
|
_ = s[i:] // 插入边界检测
_ = s[i:] // 不需要
|
2. 常量大小
访问数组(大小固定),并且索引使用某些位操作计算得到。例:
1
2
3
|
var a[17]int
_ = a[i&5] // 因为i&5 <= 5 < 17,边界检测消除
_ = a[i%5] // 不能消除,因为有可能为负数
|
3. 常量索引
使用常量索引访问。例:
1
2
3
4
|
var s[]int
if 5 < len(s) {
_ = s[5] // 5 < len(s),边界检测消除
}
|
4. 常量索引常量大小
使用常量索引访问数组。例:
1
2
|
var s[10]int
_ = s[5] // 5 < 10,边界检测消除
|
5. 迭代中索引
当使用迭代中索引时,一些边界检测可以消除。例:
1
2
3
4
5
6
|
var s[]int
for i := range s {
_ = s[i] // 消除
_ = s[i:] // 消除
_ = s[:i] // 消除
}
|
1
2
3
4
5
6
|
var s[]int
for i := 0; i < len(s); i++ {
_ = s[i] // 消除
_ = s[i:] // 消除
_ = s[:i] // 消除
}
|
1
2
3
4
5
|
var a[]int
for i := range s {
_ = s[:i+1] // 消除
_ = s[i+1:] // 消除
}
|
6. 逐渐减小的常量索引
正如概述中例子:
1
2
3
4
5
6
7
8
9
10
11
|
var s[]int
_ = s[3] // 插入边界检测
_, _, _ = s[1], s[2], s[3] // 消除
// 或
_, _, _ = s[3], s[2], s[1] // 一个边界检测
// 或
if len(s) < 3 {
_, _, _ = s[0], s[1], s[2] // 没有边界检测
}
|
7. 特殊情况
1
2
3
4
|
var s[]int
var index int
_ = s[:index] // 插入边界检测
_ = s[index:] // 插入边界检测
|
为什么上面两条赋值语句都需要边界检测?因为一个子切片可以比原来的切片大。
在Go中,s[a:b]
有两个限制:
1
2
|
0 <= a <= len(s)
a <= b <= cap(s)
|
例如:
1
2
3
4
5
6
7
8
|
s0 := make([]int, 5, 10) // len(s0) == 5, cap(s0) == 10
index := 8
_ = s0[:index] // 这行没问题
// 因为index > len(s0),所以异常了
_ = s0[index:] // panic: runtime error: slice bounds out of range
|
程序优化
我们可以利用边界检测消除优化程序性能。为了能够利用BCE,需要对程序做一点细小的调整。下面有几个例子:
例一:
1
2
3
4
5
6
7
|
func f(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // for循环内边界检测会执行多次
}
}
}
|
可以调整为:
1
2
3
4
5
6
7
8
|
func f(is []int, bs []byte) {
if len(is) >= 256 {
is = is[:256] // 避免下面for循环中的边界检测
for _, n := range bs {
_ = is[n] // 边界检测消除!
}
}
}
|
例二:
1
2
3
4
5
6
7
|
func f(isa []int, isb []int) {
if len(isa) > 0xFFF {
for _, n := range isb {
_ = isa[n & 0xFFF] // for循环内边界检测会执行多次
}
}
}
|
可以调整为:
1
2
3
4
5
6
7
8
|
func f(isa []int, isb []int) {
if len(isa) > 0xFFF {
isa = isa[:0xFFF+1] // 避免下面for循环中的边界检测
for _, n := range isb {
_ = isa[n & 0xFFF] // 边界检测消除!
}
}
}
|
例三:
1
2
3
4
5
6
7
|
func f(s []int) []int {
s2 := make([]int, len(s))
for i := range s {
s2[i] = -s[i] // 插入边界检测
}
return s2
}
|
可以调整为:
1
2
3
4
5
6
7
8
|
func f(s []int) []int {
s2 := make([]int, len(s))
s2 = s2[:len(s)] // 避免下面的边界检测
for i := range s {
s2[i] = -s[i] // 边界检测消除!
}
return s2
}
|
参考链接:
- gBounds Checking Elimination
- https://go101.org/article/bounds-check-elimination.html