从Go SDK 1.7开始,Go标准编译器开始支持边界检查消除。此优化避免了很多不必要的边界检查,从而使得编译器编译出的程序执行效率更高。
什么是边界检查?边界检查是指在运行时刻,Go运行时要检查切片和字符串的索引操作中的索引下标值是否越界了。如果越界了,就要产生一个恐慌,以维护内存安全。
虽然边界检查是维护内存安全的重要保障,但是某些索引操作如果被执行到的话,其中的索引下标值肯定不会越界。对这样的下标进行边界检查是无谓的,并会对程序执行性能产生负面影响。
下面将展示一些例子来理解Go标准编译器在什么条件下避免了边界检查。本文中的运行结果均基于Go标准编译器1.12版本。
例子1:
// example1.go
package main
func f1(s []int) {
_ = s[0] // 需要边界检查
_ = s[1] // 需要边界检查
_ = s[2] // 需要边界检查
}
func f2(s []int) {
_ = s[2] // 需要边界检查
_ = s[1] // 边界检查消除了!
_ = s[0] // 边界检查消除了!
}
func f3(s []int, index int) {
_ = s[index:] // 需要边界检查
_ = s[:index] // 边界检查消除了!
}
func main() {}
如下编译此程序,我们将获知哪些行仍然需要边界检查。
$ go build -gcflags="-d=ssa/check_bce/debug=1" example1.go
# command-line-arguments
./aa.go:5:7: Found IsInBounds
./aa.go:6:7: Found IsInBounds
./aa.go:7:7: Found IsInBounds
./aa.go:11:7: Found IsInBounds
./aa.go:17:7: Found IsSliceInBounds
从这个结果来看,函数f2
这样倒着取元素值的方式比函数f1
的效率要高,因为它避免了两个边界检查。如果函数f2
中的第一行不会越绝的话,则其中的第二行和第三行肯定也不会越界,所以第二行和第三行就不再需要边界检查了。
另外,函数f3
中的第一行如果不会越界的话,则其中的第二行肯定也不会越界。
例子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]
_ = 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() {}
从下面的编译结果来看,标准编译器消除了此例子2程序中的所有边界检查。酷!
$ go build -gcflags="-d=ssa/check_bce/debug=1" example2.go
例子3:
当前的标准编译器并非足够智能到可以消除到一切应该消除的边界检查。有时候,我们需要给标准编译器一些暗示来帮助标准编译器将这些不必要的边界检查消除掉。比如下例中的函数fd2
和函数fe2
比函数fd
和函数fe
的效率要高。
// example3.go
package main
func fd(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // 需要边界检查
}
}
}
func fd2(is []int, bs []byte) {
if len(is) >= 256 {
is = is[:256] // 给编译器一个暗示
for _, n := range bs {
_ = is[n] // 边界检查消除了!
}
}
}
func fe(isa []int, isb []int) {
if len(isa) > 0xFFF {
for _, n := range isb {
_ = isa[n & 0xFFF] // 需要边界检查
}
}
}
func fe2(isa []int, isb []int) {
if len(isa) > 0xFFF {
isa = isa[:0xFFF+1] // 给编译器一个暗示
for _, n := range isb {
_ = isa[n & 0xFFF] // 边界检查消除了!
}
}
}
func main() {}
编译输出:
$ go build -gcflags="-d=ssa/check_bce/debug=1" example3.go
# command-line-arguments
./aa.go:7:10: Found IsInBounds
./aa.go:24:11: Found IsInBounds
注意:标准编译器的每个版本都在不断地改进,所以上例中给标准编译器的暗示在以后的版本中可能将变得不再必要。
本文首发在微信Go 101公众号,欢迎各位转载本文。Go 101公众号将尽量在每个工作日发表一篇原创短文,有意关注者请扫描下面的二维码。
关于更多Go语言编程中的事实、细节和技巧,请访问《Go语言101》官方网站:https://gfw.go101.org。如果官网被墙,请访问《Go语言101》github项目:https://github.com/golang101/golang101。