1. 什么是边界检查?

边界检查,英文名 Bounds Check Elimination,简称为 BCE。它是 Go 语言中防止数组、切片越界而导致内存不安全的检查手段。如果检查下标已经越界了,就会产生 Panic。

边界检查使得我们的代码能够安全地运行,但是另一方面,也使得我们的代码运行效率略微降低。

比如下面这段代码,会进行三次的边界检查

package main

func f(s []int) {
_ = s[0] // 检查第一次
_ = s[1] // 检查第二次
_ = s[2] // 检查第三次
} func main() {}

你可能会好奇了,三次?我是怎么知道它要检查三次的。

实际上,你只要在编译的时候,加上参数即可,命令如下

go build -gcflags="-d=ssa/check_bce" demo.go
# command-line-arguments
./demo.go:4:7: Found IsInBounds
./demo.go:5:7: Found IsInBounds
./demo.go:6:7: Found IsInBounds

2. 边界检查的条件?

并不是所有的对数组、切片进行索引操作都需要边界检查。

比如下面这个示例,就不需要进行边界检查,因为编译器根据上下文已经得知,s 这个切片的长度是多少,你的终止索引是多少,立马就能判断到底有没有越界,因此是不需要再进行边界检查,因为在编译的时候就已经知道这个地方会不会 panic。

package main

func f1() {
s := []int{1, 2, 3, 4}
_ = s[:9] // 不需要边界检查 }
func main() {}

因此可以得出结论: 对于在编译阶段无法判断是否会越界的索引操作才会需要边界检查

比如这样子

package main

func f(s []int) {
_ = s[:9] // 需要边界检查
}
func main() {}

3. 边界检查的特殊案例

3.1 案例一

在如下示例代码中,由于索引 2 在最前面已经检查过会不会越界,因此聪明的编译器可以推断出后面的索引 0 和 1 不用再检查啦

 package main

func f(s []int) {
_ = s[2] // 检查一次
_ = s[1] // 不会检查
_ = s[0] // 不会检查
} func main() {}

3.2 案例二

在下面这个示例中,可以在逻辑上保证不会越界的代码,同样是不会进行越界检查的。

package main

func f(s []int) {
for index, _ := range s {
_ = s[index]
_ = s[:index+1]
_ = s[index:len(s)]
}
} func main() {}

3.3 案例三

在如下示例代码中,虽然数组的长度和容量可以确定,但是索引是通过 rand.Intn() 函数取得的随机数,在编译器看来这个索引值是不确定的,它有可能大于数组的长度,也有可能小于数组的长度。

因此第一次是需要进行检查的,有了第一次检查后,第二次索引从逻辑上就能推断,所以不会再进行边界检查。

package main

import (
"math/rand"
) func f() {
s := make([]int, 3, 3)
index := rand.Intn(3)
_ = s[:index] // 第一次检查
_ = s[index:] // 不会检查
} func main() {}

但如果把上面的代码稍微改一下,让切片的长度和容量变得不一样,结果又会变得不一样了。

package main

import (
"math/rand"
) func f() {
s := make([]int, 3, 5)
index := rand.Intn(3)
_ = s[:index] // 第一次检查
_ = s[index:] // 第二次检查
} func main() {}

只有当数组的长度和容量相等时, :index 成立,才能一定能推出 index: 也成立,这样的话,只要做一次检查即可

一旦数组的长度和容量不相等,那么 index 在编译器看来是有可能大于数组长度的,甚至大于数组的容量。

我们假设 index 取得的随机数为 4,那么它大于数组长度,此时 s[:index] 虽然可以成功,但是 s[index:] 是要失败的,因此第二次边界的检查是有必要的。

你可能会说, index 不是最大值为 3 吗?怎么可能是 4呢?

要知道编译器在编译的时候,并不知道 index 的最大值是 3 呢。

小结一下

  1. 当数组的长度和容量相等时,s[:index] 成立能够保证 s[index:] 也成立,因为只要检查一次即可
  2. 当数组的长度和容量不等时,s[:index] 成立不能保证 s[index:] 也成立,因为要检查两次才可以

3.4 案例四

有了上面的铺垫,再来看下面这个示例,由于数组是调用者传入的参数,所以编译器的编译的时候无法得知数组的长度和容量是否相等,因此只能保险一点,两个都检查。

package main

import (
"math/rand"
) func f(s []int, index int) {
_ = s[:index] // 第一次检查
_ = s[index:] // 第二次检查
} func main() {}

如果把两个表达式的顺序反过来,就只要做一次检查就行了

package main

import (
"math/rand"
) func f(s []int, index int) {
_ = s[index:] // 第一次检查
_ = s[:index] // 不用检查
} func main() {}

3.5. 主动消除边界检查

虽然编译器已经非常努力去消除一些应该消除的边界检查,但难免会有一些遗漏。

这就需要”警民合作”,对于那些编译器还未考虑到的场景,但开发者又极力追求程序的运行效率的,可以使用一些小技巧给出一些暗示,告诉编译器哪些地方可以不用做边界检查。

比如下面这个示例,从代码的逻辑上来说,是完全没有必要做边界检查的,但是编译器并没有那么智能,实际上每个for循环,它都要做一次边界的检查,非常的浪费性能。

package main

func f(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // 每个循环都要边界检查
}
}
}
func main() {}

可以试着在 for 循环前加上这么一句 is = is[:256] 来告诉编译器新 is 的长度为 256,最大索引值为 255,不会超过 byte 的最大值,因为 is[n] 从逻辑上来说是一定不会越界的。

package main

func f(is []int, bs []byte) {
if len(is) >= 256 {
is = is[:256]
for _, n := range bs {
_ = is[n] // 不需要做边界检查
}
}
}
func main() {}

3.6 边界检查对性能的影响

一直在讨论边界检查对性能的影响,但是到底影响有多大呢? 不妨以上面的例子做一个基准测试

package main

import "testing"

func f4(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // 每个循环都要边界检查
}
}
} func f5(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
is = is[:256]
_ = is[n] // 每个循环都要边界检查
}
}
} func BenchmarkFunc_f4_test(b *testing.B) {
s := make([]int, 1000, 10000000)
bs := []byte{'b', 'a', 'c', 'd', 'e', 'g', 'h', 'j'}
for i := 0; i < b.N; i++ {
f4(s, bs)
}
} func BenchmarkFunc_f5_test(b *testing.B) {
s := make([]int, 1000, 10000000)
bs := []byte{'b', 'a', 'c', 'd', 'e', 'g', 'h', 'j'}
for i := 0; i < b.N; i++ {
f5(s, bs)
}
}

运行基准测试结果如下:

go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: Go_base/daily_test/bce_demo
BenchmarkFunc_f4_test-8 179074254 6.33 ns/op 0 B/op 0 allocs/op
BenchmarkFunc_f5_test-8 208692784 5.82 ns/op 0 B/op 0 allocs/op
PASS
ok Go_base/daily_test/bce_demo 3.253s

如上结果,随着for循环次数的增加,其性能有了明显的差异,对于小的切片,数组操作时可能效果并不是很明显,但是如果涉及到数据比较大,或者性能比较严苛的地方,避免边界检查还是很有必要的。

四. 参考

  1. https://iswbm.com/362.html
  2. https://gfw.go101.org/article/bounds-check-elimination.html

Go 性能提升tips--边界检查的更多相关文章

  1. Web 应用性能提升 10 倍的 10 个建议

    转载自http://blog.jobbole.com/94962/ 提升 Web 应用的性能变得越来越重要.线上经济活动的份额持续增长,当前发达世界中 5 % 的经济发生在互联网上(查看下面资源的统计 ...

  2. 再谈HTTP2性能提升之背后原理—HTTP2历史解剖

    即使千辛万苦,还是把网站升级到http2了,遇坑如<phpcms v9站http升级到https加http2遇到到坑>. 因为理论相比于 HTTP 1.x ,在同时兼容 HTTP/1.1 ...

  3. Android应用程序性能优化Tips

    对于我们设计的应用需要做到以下特征:build an app that's smooth, responsive(反应敏捷), and uses as little battery as possib ...

  4. 有史以来性价比最高最让人感动的一次数据库&amp;SQL优化(DB &amp; SQL TUNING)——半小时性能提升千倍

    昨天,一个客户现场人员急急忙忙打电话找我,说需要帮忙调优系统,因为经常给他们干活,所以,也就没多说什么,先了解情况,据他们说,就是他们的系统最近才出现了明显的反应迟钝问题,他们的那个系统我很了解,软硬 ...

  5. Web 应用性能提升的 10 个建议

    建议一.利用反向代理服务器加速和保护应用 如果 Web 应用运行在一台独立的电脑上,性能问题的解决方案是显而易见的:换一台更快的电脑,里面加上更多的处理器.内存.快速磁盘阵列等等.然后在这台新电脑上运 ...

  6. 禁用 Python GC,Instagram 性能提升10%

    通过关闭 Python 垃圾收集(GC)机制,该机制通过收集和释放未使用的数据来回收内存,Instagram 的运行效率提高了 10 %.是的,你没听错!通过禁用 GC,我们可以减少内存占用并提高 C ...

  7. php 性能优化之opcache - 让你的php性能提升 50%

    性能提升原理:减少文件解析的时间. 我们都知道,程序要运行,得有一个编译或者解析的过程,编译或解析之后的代码才是机器可以运行的. 而 php 是一种解析性语言,在使用php来处理http请求的时候,每 ...

  8. Web性能优化系列:10个JavaScript性能提升的技巧

    由 伯乐在线 - Delostik 翻译,黄利民 校稿.未经许可,禁止转载!英文出处:jonraasch.com.欢迎加入翻译小组. Nicholas Zakas是一位 JS 大师,Yahoo! 首页 ...

  9. Spring Boot 2.2 正式发布,大幅性能提升 + Java 13 支持

    之前 Spring Boot 2.2没能按时发布,是由于 Spring Framework 5.2 的发布受阻而推迟.这次随着 Spring Framework 5.2.0 成功发布之后,Spring ...

随机推荐

  1. NKOJ-2936 城市建设

    问题描述: PS国是一个拥有诸多城市的大国,国王Louis为城市的交通建设可谓绞尽脑汁.Louis可以在某些城市之间修建道路,在不同的城市之间修建道路需要不同的花费.Louis希望建造最少的道路使得国 ...

  2. RAW RGB格式

    RAW RGB格式 10bit Raw RGB, 就是说用10bit去表示一个R, G, 或者B, 通常的都是用8bit的. 所以你后面处理时要把它转换为8bit的, 比较简单的方法就是将低两位去掉, ...

  3. MIPI的走线阻抗

    MIPI的走线阻抗100欧的要求是根据LVDS(Low Voltage Differential Signaling)电平定义的. LVDS差分信号PN两线最大幅度是350mV,内部一个恒流源电流是3 ...

  4. Spring Boot 2.5.0 重新设计的spring.sql.init 配置有何用?

    前几天Spring Boot 2.5.0发布了,其中提到了关于Datasource初始化机制的调整,有读者私信想了解这方面做了什么调整.那么今天就要详细说说这个重新设计的配置内容,并结合实际情况说说我 ...

  5. hdu 2189 来生一起走(DP)

    题意: 有N个志愿者.指挥部需要将他们分成若干组,但要求每个组的人数必须为素数.问不同的方案总共有多少.(N个志愿者无差别,即每个组的惟一标识是:人数) 思路: 假设N个人可分为K组,将这K组的人数从 ...

  6. newusers 拷贝服务器A上的用户,批量添加到其它服务器

    服务器B 需要添加多个用户,要求与服务器A 的用户列表一致 1.拷贝服务器A 上的 /etc/passwd 中用户信息,用user1-10为例 #grep ^user /etc/passwd > ...

  7. MongoDB 集群 config server 查询超时导致 mongos 集群写入失败

    环境 OS:CentOS 7.x DB:MongoDB 3.6.12 集群模式:mongod-shard1 *3 + mongod-shard2 *3 + mongod-conf-shard *3 + ...

  8. idea连接数据库时区:Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' prope

    错误界面 IDEA连接mysql,地址,用户名,密码,数据库名,全都配置好了,点测试连接,咔!不成功! 界面是这样的, 翻译过来就是:服务器返回无效时区.进入"高级"选项卡,手动设 ...

  9. uni-app app端设置全屏背景色

    设置page:{样式},博主调试的时候在app端不起作用,设置配置文件的backgroundColor也没有用,所以博主就使用了一个稍微比较偏的办法解决了,没有用获取设备信息的api来实现 具体操作就 ...

  10. LeetCode 199. 二叉树的右视图 C++ 用时超100%

    /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode ...