[译] Bounds Check Elimination 边界检查消除

[译] Bounds Check Elimination 边界检查消除

Go 是一种内存安全的语言,在针对数组 (array) 或 Slice 做索引和切片操作时,Go 的运行时(runtime)会检查所涉及的索引是否超出范围。如果索引超出范围,将产生一个 Panic,以防止无效索引造成的伤害。这就是边界检查(BCE)。边界检查使我们的代码能够安全地运行,但也会影响一定的性能。

原文链接:
Bounds Check Elimination

自从 Go Toolchain 1.7 以后,标准的 Go 编译器采用了一个基于 SSA (静态单赋值形式)的新的编译器后端。SSA 帮助 Go 编译器有效地进行代码优化,比如 BCE (边界检查消除) 和 CSE (公共子表达式消除)。BCE 可以避免一些不必要的边界检查,CSE 可以避免一些重复的计算,如此使得标准的 Go 编译器可以生成更高效的程序。有时这些优化的改进效果是显而易见的。

本文将列出一些示例,说明 BCE 如何与标准的 Go 编译器1.7 + 协同工作。

对于 Go Toolchain 1.7 + ,我们可以使用 -gcflags = “-d=ssa/check _ bce/debug=1”编译器标志来显示哪些代码行仍然需要进行边界检查。

例 1

// example1.go
package mainfunc f1(s []int) {_ = s[0] // line 5: 需要边界检查_ = s[1] // line 6: 需要边界检查_ = s[2] // line 7: 需要边界检查
}func f2(s []int) {_ = s[2] // line 11: 需要边界检查_ = s[1] // line 12: 边界检查被消除_ = s[0] // line 13: 边界检查被消除
}func f3(s []int, index int) {_ = s[index] // line 17: 需要边界检查_ = s[index] // line 18: 边界检查被消除
}func f4(a [5]int) {_ = a[4] // line 22: 边界检查被消除
}func main() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" example1.go
./example1.go:5: Found IsInBounds
./example1.go:6: Found IsInBounds
./example1.go:7: Found IsInBounds
./example1.go:11: Found IsInBounds
./example1.go:17: Found IsInBounds

我们可以看到,没有必要为函数 f2 中的第 12 行和第 13 行进行边界检查,因为第 11 行的边界检查确保了第 12 行和第 13 行的索引不会超出范围。

但在函数 f1 中,必须对这三行都进行边界检查。因为第 5 行的边界检查不能保证第六行和第七行的安全,同样第六行的检查也不能保证第七行的安全。

而对于函数 f3,编译器知道如果第一个 s [ index ] 是安全的,那么第二个 s [ index ] 就也是绝对安全的。

编译器还能正确地判断出 f4 中的唯一一行(22行)是安全的。

例 2

// example2.go
package mainfunc 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() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" example2.go

酷! 标准编译器删除程序中的所有绑定检查。

注意: 在 Go Toolchain 1.11 版本之前,标准编译器不够智能,无法检测到第22行是安全的。

例3

// example3.go
package mainimport "math/rand"func fa() {s := []int{0, 1, 2, 3, 4, 5, 6}index := rand.Intn(7)_ = s[:index] // line 9: bounds check_ = s[index:] // line 10: bounds check eliminated!
}func fb(s []int, i int) {_ = s[:i] // line 14: bounds check_ = s[i:] // line 15: bounds check, not smart enough?
}func fc() {s := []int{0, 1, 2, 3, 4, 5, 6}s = s[:4]i := rand.Intn(7)_ = s[:i] // line 22: bounds check_ = s[i:] // line 23: bounds check, not smart enough?
}func main() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" example3.go
./example3.go:9: Found IsSliceInBounds
./example3.go:14: Found IsSliceInBounds
./example3.go:15: Found IsSliceInBounds
./example3.go:22: Found IsSliceInBounds
./example3.go:23: Found IsSliceInBounds

哦,这么多地方还需要做边界检查!

但是,为什么标准的 Go 编译器认为第 10 行是安全的,而第 15 行和第 23 行却不是呢?编译器还不够聪明吗?

事实上,编译器设计如此!为什么?原因是子切片表达式中的起始索引可能大于原始切片的长度。让我们看一个简单的例子:

package mainfunc main() {s0 := make([]int, 5, 10) // len(s0) == 5, cap(s0) == 10index := 8// 在 go 中,对于子切片语法 s[a:b] 必须保证 0 <= a <= b <= cap(s)// 否则会引起 panic_ = s0[:index]// 上面一行是安全的,但不能保证下面一行也是安全的// 事实上,下面一行将会导致 panic_ = s0[index:] // panic
}

因此,只有满足 len(s) == cap(s) 时,才能根据 s[:index] 是安全的得出 s[index:] 也是安全地的结论,这就是为什么函数 fbfc 中的代码行仍然需要进行边界检查的原因。

标准 Go 编译器成功地检测到函数 fa 中的 len (s) 等于 cap (s) 干得好! Go团队加油!

例4

// example4.go
package mainimport "math/rand"func fb2(s []int, index int) {_ = s[index:] // line 7: bounds check_ = s[:index] // line 8: bounds check eliminated!
}func fc2() {s := []int{0, 1, 2, 3, 4, 5, 6}s = s[:4]index := rand.Intn(7)_ = s[index:] // line 15 bounds check_ = s[:index] // line 16: bounds check eliminated!
}func main() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" example4.go
./example4.go:7:7: Found IsSliceInBounds
./example4.go:15:7: Found IsSliceInBounds

在这个例子中,go 编译器成功推断出:

  • 如果第 7 行是安全的,那么第 8 行也是安全地
  • 如果第 15 行是安全的,那么第 16 行也是安全地

注意:在1.9版本之前的 Go Toolchain 中,标准的 Go 编译器无法检测到第 8 行不需要边界检查。

例 5

当前版本的标准 Go 编译器不够聪明,无法消除所有不必要的边界检查。有时,我们可以做一些提示来帮助编译器消除一些不必要的边界检查.

// example5.go
package mainfunc fd(is []int, bs []byte) {if len(is) >= 256 {for _, n := range bs {_ = is[n] // line 7: bounds check}}
}func fd2(is []int, bs []byte) {if len(is) >= 256 {is = is[:256] // line 14: a hintfor _, n := range bs {_ = is[n] // line 16: BCEed!}}
}func fe(isa []int, isb []int) {if len(isa) > 0xFFF {for _, n := range isb {_ = isa[n & 0xFFF] // line 24: bounds check}}
}func fe2(isa []int, isb []int) {if len(isa) > 0xFFF {isa = isa[:0xFFF+1] // line 31: a hintfor _, n := range isb {_ = isa[n & 0xFFF] // line 33: BCEed!}}
}func main() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" example5.go
./example5.go:7: Found IsInBounds
./example5.go:24: Found IsInBounds

核心的思想就是尽量消除在循环中的边界检查,这个例子有点奇怪,可以看下面这个:

// example4.go
package mainfunc bad(a, b []int64, n int) {if len(a) >= n && len(b) >= n {for i, v := range b {a[i] = v}}
}func good(a, b []int64, n int) {if len(a) >= n && len(b) >= n {a = a[:n]b = b[:n]for i, v := range b {a[i] = v}}
}func main() {}
$ go run -gcflags="-d=ssa/check_bce/debug=1" .\example2.go
# command-line-arguments
.\example2.go:7:5: Found IsInBounds
.\example2.go:14:8: Found IsSliceInBounds

通过 14 15 行的子切片操作,我们可以把边界检查放到循环之外,简单跑一下 Benchmark 差距还是挺明显的

cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz
BenchmarkBCE
BenchmarkBCE/good
BenchmarkBCE/good-4         	  296671	      4912 ns/op
BenchmarkBCE/bad
BenchmarkBCE/bad-4          	  182302	      6136 ns/op
PASS

摘要

标准的 Go 编译器进行了更多的 BCE 优化。它们可能不像上面列出的那么明显,所以本文不会全部展示。

尽管标准 Go 编译器中的 BCE 特性仍然不够完美,但对于许多常见情况来说,它确实做得很好。毫无疑问,标准的 Go 编译器在以后的版本中会做得更好,这样上面第5个例子中的提示可能就没有必要了。谢谢团队增加了这个美妙的功能!

参考文献

  1. Bounds Check Elimination
  2. Utilizing the Go 1.7 SSA Compiler (and the second part)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/457383.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

cad多段线画圆弧方向_CAD箭头怎么画

CAD箭头怎么画问&#xff1a;CAD箭头怎么画&#xff1f;答&#xff1a;想要回答CAD箭头怎么画这个问题&#xff0c;得先从CAD多段线命令说起&#xff0c;画箭只是多段线的一种应用。执行CAD多段线命令的三种方式1.单击菜单栏上的"绘图">>"多段线"。2…

HDU 5410 CRB and His Birthday ——(完全背包变形)

对于每个物品&#xff0c;如果购买&#xff0c;价值为A[i]*xB[i]的背包问题。 先写了一发是WA的 。代码如下&#xff1a; 1 #include <stdio.h>2 #include <algorithm>3 #include <string.h>4 #include <set>5 using namespace std;6 typedef pair<…

一篇讲Java指令重排和内存可见性的好文

在这里&#xff1a; http://tech.meituan.com/java-memory-reordering.html 指令重排和内存可见性&#xff08;缓存不一致&#xff09;是两个不同的问题。 volatile关键字太强&#xff0c;即阻挡指令重排&#xff0c;又保证内存一致性。 unsafe.putOrderedXXX()只阻挡指令重排&…

php 获取delete蚕丝_php结合Redis实现100万用户投票项目,并实时查看到投票情况的案例...

场景&#xff1a;某网站需要对其项目做一个投票系统&#xff0c;投票项目上线后一小时之内预计有100万用户进行投票&#xff0c;希望用户投票完就能看到实时的投票情况这个场景可以使用redismysql冷热数据交换来解决。何为冷热数据交换&#xff1f;冷数据&#xff1a;之前使用的…

硬件内存模型 Hardware Memory Models

硬件内存模型 Hardware Memory Models (Memory Models, Part 1) Posted on Tuesday, June 29, 2021. 简介&#xff1a;童话的终结 很久以前&#xff0c;当人们还在写单线程程序的时候&#xff0c;让程序跑的更快的一个最有效的办法就是什么也不做&#xff0c;因为下一代硬件…

碰到日期题就怕的我来写一道水题吧

HDOJ-2005&#xff0c; http://acm.hdu.edu.cn/showproblem.php?pid2005 20XX系列的水题哈哈&#xff0c;写了二十分钟&#xff0c;就为找到一种比较正常不傻逼的写法。。。 嗯&#xff0c;学习了一下&#xff0c;闰年的判断可以写成一个接受参数的宏。 #define lev(n) (n%40&…

判断是否为gif/png图片的正确姿势

判断是否为gif/png图片的正确姿势 1.在能取到图片后缀的前提下 123456789//假设这是一个网络获取的URLNSString *path "http://pic3.nipic.com/20090709/2893198_075124038_2.gif";// 判断是否为gifNSString *extensionName path.pathExtension;if ([extensionName…

【Go】Map 的空间利用率统计

Go 中 map 利用率 今天刷 B 站看见有 Up 主在讲布隆过滤器&#xff0c;提到了利用率的问题&#xff0c;假设有一组数据&#xff0c;范围分布非常广&#xff0c;使用布隆过滤器时如何尽量少的减少内存使用&#xff0c;感觉除了针对特定数据的定向优化外没什么特别好的办法&…

ap模式和sta模式共存_AP+AC组网下的本地转发及集中转发

现在越来越多的企业都有自己的无线网络&#xff0c;而无线网络的组网方式一般都是使用ACAP模式进行组网&#xff0c;使用无线网络能够提供经济、高效的网络接入方式。相比有线网络&#xff0c;无线网络下只要能接入无线网的地方都可以使用网络&#xff0c;用户可以自由移动。而…

《JS权威指南学习总结--6.7属性的特性》

内容要点&#xff1a; 一.ES5中查询和设置属性的API 1.可以通过这些API给原型对象添加方法&#xff0c;并将它们设置成不可枚举的&#xff0c;这让它们看起来更像内置方法。 2.可以通过这些API给对象定义不能修改或删除的属性&#xff0c;借此 "锁定" 这个对象。 3.数…

【干货分享】流程DEMO-事务呈批表

流程名&#xff1a; 事务呈批表 业务描述&#xff1a; 办公采购、会议费用等事务的申请。流程发起时&#xff0c;会检查预算&#xff0c;如果预算不够&#xff0c;将不允许发起费用申请&#xff0c;如果预算够用&#xff0c;将发起流程&#xff0c;同时占用相应金额的预算&…

【译】TcMalloc: Thread-Caching Malloc

TcMalloc 的核心是分层缓存&#xff0c;前端没有锁竞争&#xff0c;可以快速分配和释放较小的内存对象&#xff08;一般是 256 KB&#xff09;前端有两种实现&#xff0c;分别是 pre-CPU 和 pre-Thread 模式&#xff0c;前者申请一块大的连续内存&#xff0c;每一个逻辑 CPU 将…

kotlin编译失败_Kotlin使用GraalVM开发原生命令行应用

背景之前用kotlin开发过一款根据建表DDL语句生成plantuml ER图的应用。被问如何使用&#xff0c;答曰"给你一个jar包&#xff0c;然后执行java -jar ddl2plantuml.jar ./ddl.sql ./er.puml 就可以了。是不是so easy?"结果被吐槽了一番&#xff0c;为什么不能像命令行…

Swift - 添加纯净的Alamofire

Swift - 添加纯净的Alamofire 如果你有代码洁癖,不能容忍任何多余的东西,请继续往下看. 1. 下载Alamofire (https://github.com/Alamofire/Alamofire) 2. 解压缩并打开 Alamofire.xcworkspace 3. 删除不必要的内容 (根据你的需求自己定) 4. 顺便把文件夹里面的无关内容也删除掉…

jquery 获取系统默认年份_你没有看错,爬网页数据,C# 也可以像 Jquery 那样

一&#xff1a;背景1. 讲故事前段时间搞了一个地方性民生资讯号&#xff0c;资讯嘛&#xff0c;都是我抄你的&#xff0c;你抄官媒的&#xff0c;小市民都喜欢奇闻异事&#xff0c;所以就存在一个需求&#xff0c;如何去定向抓取奇闻异事的地方号上的新闻&#xff0c;其实做起来…

linux下怎么编译运行C语言程序?

linux下的C语言编译器是gcc&#xff0c;C的编译器是g。 linux下编程可以使用编辑器vi或vim&#xff0c;建议使用vim&#xff0c;因为它有语法高亮显示。程序编写好后&#xff0c;假设你的程序名为test.c&#xff0c;可以使用gcc -o test test.c。test就是编译好的可执行程序./t…

undertow 怎么创建线程_为什么很多SpringBoot开发者放弃了Tomcat,选择了Undertow

点击上方“后端技术精选”&#xff0c;选择“置顶公众号”技术文章第一时间送达&#xff01;作者&#xff1a;阿迈达toutiao.com/a6775476659416990212/前言在SpringBoot框架中&#xff0c;我们使用最多的是Tomcat&#xff0c;这是SpringBoot默认的容器技术&#xff0c;而且是内…

一起玩转CoordinatorLayout

作为Material Design风格的重要组件,CoordinatorLayout协调多种组件的联动&#xff0c;实现各种复杂的效果&#xff0c;在实际项目中扮演着越来越重要的角色。本篇博客将由浅到深&#xff0c;带你一起玩转CoordinatorLayout。 官方文档对CoordinatorLayout是这样描述的&#xf…

离散数学图论旅行规划问题_2020年MathorCup高校数学建模挑战赛——C 题 仓内拣货优化问题...

下面的链接是精华版思路&#xff0c;亮点是对第六问的探讨。高度概括一下&#xff1a;第一问曼哈顿&#xff0c;第二问用免疫&#xff0c;三问增加任务单&#xff0c;四问增加拣货员&#xff0c;五问改变复核台&#xff0c;六问亮点来探讨~ 有点皮MathorCup C题 仓内拣货优化问…

Asp.NetWebForm的控件属性

一&#xff1a;GridView&#xff1a; 1.绑定ID 的值&#xff1a;DataKeyNames"Id", 2.自动产生列的意思:AutoGenerateColumns 3.如何注册脚本&#xff1a;ClientScript.RegisterStartupScript(this.GetType(),"text","alert(删除成功)"&#xf…