Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
go开源项目镜像
GolangCodingTime
提交
9d86c317
G
GolangCodingTime
项目概览
go开源项目镜像
/
GolangCodingTime
通知
0
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
G
GolangCodingTime
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
9d86c317
编写于
4月 11, 2021
作者:
写代码的明哥
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
添加文章:边界检查
上级
56f88e0d
变更
2
隐藏空白更改
内联
并排
Showing
2 changed file
with
506 addition
and
0 deletion
+506
-0
source/c07/c07_02.md
source/c07/c07_02.md
+239
-0
source/c07/c07_02.rst
source/c07/c07_02.rst
+267
-0
未找到文件。
source/c07/c07_02.md
0 → 100644
浏览文件 @
9d86c317
# 7.2 Go 语言中边界检查
![](
http://image.iswbm.com/20200607145423.png
)
## 1. 什么是边界检查?
边界检查,英文名
`Bounds Check Elimination`
,简称为 BCE。它是 Go 语言中防止数组、切片越界的检查手段。如果检查下标已经越界了,就会产生 Panic。
边界检查使得我们的代码能够安全地运行,但是另一方面,也使得我们的代码运行效率略微降低。
比如下面这段代码,会进行三次的边界检查
```
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[
0
]
// 检查第一次
_
=
s
[
1
]
// 检查第二次
_
=
s
[
2
]
// 检查第三次
}
func
main
()
{}
```
你可能会好奇了,三次?我是怎么知道它要检查三次的。
实际上,你只要在编译的时候,加上参数即可,命令如下
```
shell
$
go build
-gcflags
=
"-d=ssa/check_bce/debug=1"
main.go
# command-line-arguments
./main.go:4:7: Found IsInBounds
./main.go:5:7: Found IsInBounds
./main.go:6:7: Found IsInBounds
```
## 2. 边界检查的条件?
并不是所有的对数组、切片进行索引操作都需要边界检查。
比如下面这个示例,就不需要进行边界检查,因为编译器根据上下文已经得知,
`s`
这个切片的长度是多少,你的终止索引是多少,立马就能判断到底有没有越界,因此是不需要再进行边界检查,因为在编译的时候就已经知道这个地方会不会 panic。
```
go
package
main
func
f
()
{
s
:=
[]
int
{
1
,
2
,
3
,
4
}
_
=
s
[
:
9
]
// 不需要边界检查
}
func
main
()
{}
```
因此可以得出结论,对于在编译阶段无法判断是否会越界的索引操作才会需要边界检查,比如这样子
```
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[
:
9
]
// 需要边界检查
}
func
main
()
{}
```
## 3. 边界检查的特殊案例
### 3.1 案例一
在如下示例代码中,由于索引 2 在最前面已经检查过会不会越界,因此聪明的编译器可以推断出后面的索引 0 和 1 不用再检查啦
```
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[
2
]
// 检查一次
_
=
s
[
1
]
// 不会检查
_
=
s
[
0
]
// 不会检查
}
func
main
()
{}
```
### 3.2 案例二
在下面这个示例中,可以在逻辑上保证不会越界的代码,同样是不会进行越界检查的。
```
go
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()`
函数取得的随机数,在编译器看来这个索引值是不确定的,它有可能大于数组的长度,也有可能小于数组的长度。
因此第一次是需要进行检查的,有了第一次检查后,第二次索引从逻辑上就能推断,所以不会再进行边界检查。
```
go
package
main
import
(
"math/rand"
)
func
f
()
{
s
:=
make
([]
int
,
3
,
3
)
index
:=
rand
.
Intn
(
3
)
_
=
s
[
:
index
]
// 第一次检查
_
=
s
[
index
:
]
// 不会检查
}
func
main
()
{}
```
但如果把上面的代码稍微改一下,让切片的长度和容量变得不一样,结果又会变得不一样了。
```
go
package
main
import
(
"math/rand"
)
func
fa
()
{
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 案例四
有了上面的铺垫,再来看下面这个示例,由于数组是调用者传入的参数,所以编译器的编译的时候无法得知数组的长度和容量是否相等,因为只能保险一点,两个都检查。
```
go
package
main
import
(
"math/rand"
)
func
fb
(
s
[]
int
,
index
int
)
{
_
=
s
[
:
index
]
// 第一次检查
_
=
s
[
index
:
]
// 第二次检查
}
func
main
()
{}
```
但是如果把两个表达式的顺序反过来,就只要做一次检查就行了,原因我就不赘述了。
```
go
package
main
import
(
"math/rand"
)
func
fb
(
s
[]
int
,
index
int
)
{
_
=
s
[
index
:
]
// 第一次检查
_
=
s
[
:
index
]
// 不用检查
}
func
main
()
{}
```
## 5. 主动消除边界检查
虽然编译器已经非常努力去消除一些应该消除的边界检查,但难免会有一些遗漏。
这就需要"警民合作",对于那些编译器还未考虑到的场景,但开发者又极力追求程序的运行效率的,可以使用一些小技巧给出一些暗示,告诉编译器哪些地方可以不用做边界检查。
比如下面这个示例,从代码的逻辑上来说,是完全没有必要做边界检查的,但是编译器并没有那么智能,实际上每个for循环,它都要做一次边界的检查,非常的浪费性能。
```
go
package
main
func
fd
(
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]`
从逻辑上来说是一定不会越界的。
```
go
package
main
func
fd
(
is
[]
int
,
bs
[]
byte
)
{
if
len
(
is
)
>=
256
{
is
=
is
[
:
256
]
for
_
,
n
:=
range
bs
{
_
=
is
[
n
]
// 不需要做边界检查
}
}
}
func
main
()
{}
```
## 参考文档
-
[
边界检查消除
](
https://gfw.go101.org/article/bounds-check-elimination.html
)
![](
http://image.iswbm.com/20200607174235.png
)
\ No newline at end of file
source/c07/c07_02.rst
0 → 100644
浏览文件 @
9d86c317
7.2
Go
语言中边界检查
=====================
|
image0
|
1.
什么是边界检查?
-------------------
边界检查,英文名
``
Bounds
Check
Elimination
``\
,简称为
BCE
。它是
Go
语言中防止数组、切片越界的检查手段。如果检查下标已经越界了,就会产生
Panic
。
边界检查使得我们的代码能够安全地运行,但是另一方面,也使得我们的代码运行效率略微降低。
比如下面这段代码,会进行三次的边界检查
..
code
::
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[
0
]
//
检查第一次
_
=
s
[
1
]
//
检查第二次
_
=
s
[
2
]
//
检查第三次
}
func
main
()
{}
你可能会好奇了,三次?我是怎么知道它要检查三次的。
实际上,你只要在编译的时候,加上参数即可,命令如下
..
code
::
shell
$
go
build
-
gcflags
=
"-d=ssa/check_bce/debug=1"
main
.
go
#
command
-
line
-
arguments
./
main
.
go
:
4
:
7
:
Found
IsInBounds
./
main
.
go
:
5
:
7
:
Found
IsInBounds
./
main
.
go
:
6
:
7
:
Found
IsInBounds
2.
边界检查的条件?
-------------------
并不是所有的对数组、切片进行索引操作都需要边界检查。
比如下面这个示例,就不需要进行边界检查,因为编译器根据上下文已经得知,
\
``
s
``
这个切片的长度是多少,你的终止索引是多少,立马就能判断到底有没有越界,因此是不需要再进行边界检查,因为在编译的时候就已经知道这个地方会不会
panic
。
..
code
::
go
package
main
func
f
()
{
s
:=
[]
int
{
1
,
2
,
3
,
4
}
_
=
s
[:
9
]
//
不需要边界检查
}
func
main
()
{}
因此可以得出结论,对于在编译阶段无法判断是否会越界的索引操作才会需要边界检查,比如这样子
..
code
::
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[:
9
]
//
需要边界检查
}
func
main
()
{}
3.
边界检查的特殊案例
---------------------
3.1
案例一
~~~~~~~~~~
在如下示例代码中,由于索引
2
在最前面已经检查过会不会越界,因此聪明的编译器可以推断出后面的索引
0
和
1
不用再检查啦
..
code
::
go
package
main
func
f
(
s
[]
int
)
{
_
=
s
[
2
]
//
检查一次
_
=
s
[
1
]
//
不会检查
_
=
s
[
0
]
//
不会检查
}
func
main
()
{}
3.2
案例二
~~~~~~~~~~
在下面这个示例中,可以在逻辑上保证不会越界的代码,同样是不会进行越界检查的。
..
code
::
go
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
()``
函数取得的随机数,在编译器看来这个索引值是不确定的,它有可能大于数组的长度,也有可能小于数组的长度。
因此第一次是需要进行检查的,有了第一次检查后,第二次索引从逻辑上就能推断,所以不会再进行边界检查。
..
code
::
go
package
main
import
(
"math/rand"
)
func
f
()
{
s
:=
make
([]
int
,
3
,
3
)
index
:=
rand
.
Intn
(
3
)
_
=
s
[:
index
]
//
第一次检查
_
=
s
[
index
:]
//
不会检查
}
func
main
()
{}
但如果把上面的代码稍微改一下,让切片的长度和容量变得不一样,结果又会变得不一样了。
..
code
::
go
package
main
import
(
"math/rand"
)
func
fa
()
{
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
案例四
~~~~~~~~~~
有了上面的铺垫,再来看下面这个示例,由于数组是调用者传入的参数,所以编译器的编译的时候无法得知数组的长度和容量是否相等,因为只能保险一点,两个都检查。
..
code
::
go
package
main
import
(
"math/rand"
)
func
fb
(
s
[]
int
,
index
int
)
{
_
=
s
[:
index
]
//
第一次检查
_
=
s
[
index
:]
//
第二次检查
}
func
main
()
{}
但是如果把两个表达式的顺序反过来,就只要做一次检查就行了,原因我就不赘述了。
..
code
::
go
package
main
import
(
"math/rand"
)
func
fb
(
s
[]
int
,
index
int
)
{
_
=
s
[
index
:]
//
第一次检查
_
=
s
[:
index
]
//
不用检查
}
func
main
()
{}
5.
主动消除边界检查
-------------------
虽然编译器已经非常努力去消除一些应该消除的边界检查,但难免会有一些遗漏。
这就需要“警民合作”,对于那些编译器还未考虑到的场景,但开发者又极力追求程序的运行效率的,可以使用一些小技巧给出一些暗示,告诉编译器哪些地方可以不用做边界检查。
比如下面这个示例,从代码的逻辑上来说,是完全没有必要做边界检查的,但是编译器并没有那么智能,实际上每个
for
循环,它都要做一次边界的检查,非常的浪费性能。
..
code
::
go
package
main
func
fd
(
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
]``
从逻辑上来说是一定不会越界的。
..
code
::
go
package
main
func
fd
(
is
[]
int
,
bs
[]
byte
)
{
if
len
(
is
)
>=
256
{
is
=
is
[:
256
]
for
_
,
n
:=
range
bs
{
_
=
is
[
n
]
//
不需要做边界检查
}
}
}
func
main
()
{}
参考文档
--------
-
`
边界检查消除
<
https
://
gfw
.
go101
.
org
/
article
/
bounds
-
check
-
elimination
.
html
>`
__
|
image1
|
..
|
image0
|
image
::
http
://
image
.
iswbm
.
com
/
20200607145423.
png
..
|
image1
|
image
::
http
://
image
.
iswbm
.
com
/
20200607174235.
png
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录