什么是边界检查

  1. 边界检查,英文名Bounds Check Elimination,简称为 BCE。
  2. 它是Go语言中防止数组切片越界而导致内存不安全的检查手段。如果检查下标已经越界了,就会产生Panic
  3. 边界检查使得我们的代码能够安全地运行,但是另一方面,也使得我们的代码运行效率略微降低。
  4. 比如下面这段代码,会进行三次的边界检查。
1
2
3
4
5
6
7
8
9
package main

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

func main() {}
  1. 你可能会好奇了,三次?我是怎么知道它要检查三次的。
  2. 实际上,你只要在编译的时候,加上参数-gcflags="-d=ssa/check_bce/debug=1"即可,命令如下:
1
2
3
4
5
$ 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

边界检查的条件

  1. 并不是所有的对数组、切片进行索引操作都需要边界检查。
  2. 比如下面这个示例,就不需要进行边界检查,因为编译器根据上下文已经得知,s这个切片的长度是多少,你的终止索引是多少,立马就能判断到底有没有越界,因此是不需要再进行边界检查,因为在编译的时候就已经知道这个地方会不会 panic。
1
2
3
4
5
6
7
8
9
package main

func f() {
    s := []int{1,2,3,4}
    // panic: runtime error: slice bounds out of range [:9] with capacity 4
    _ = s[:9]  // 不需要边界检查
}

func main()  {}
  1. 因此可以得出结论,对于在编译阶段无法判断是否会越界的索引操作才会需要边界检查,比如这样子:
1
2
3
4
5
6
7
package main

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

func main()  {}

边界检查的特殊案例

案例一

  1. 在如下示例代码中,由于索引2在最前面已经检查过会不会越界,因此聪明的编译器可以推断出后面的索引0和1不用再检查。
1
2
3
4
5
6
7
8
9
 package main

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

func main() {}

案例二

  1. 在下面这个示例中,可以在逻辑上保证不会越界的代码,同样是不会进行越界检查的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

// [low:high:max]
//  len = high - low
//  cap = max - low

// s => len = 10, cap = 20

func f(s []int) {
    for index, _ := range s {
        // 以下操作都是在有效的索引范围
        _ = s[index]
        _ = s[:index+1]     // index [0,9]
        _ = s[index:len(s)] // len(s) = 10
    }
}

func main()  {}

案例三

  1. 在如下示例代码中,虽然数组的长度和容量可以确定,但是索引是通过rand.Intn()函数取得的随机数,在编译器看来这个索引值是不确定的,它有可能大于数组的长度,也有可能小于数组的长度。
  2. 因此第一次是需要进行检查的,有了第一次检查后,第二次索引从逻辑上就能推断,所以不会再进行边界检查。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
    "math/rand"
)

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

func main()  {}
  1. 我们只有当数组的长度和容量相等时,:index成立,才能一定能推出index:也成立,这样的话,只要做一次检查即可。
  2. 一旦数组的长度和容量不相等,那么index在编译器看来是有可能大于数组长度的,甚至大于数组的容量。
  3. 我们假设index取得的随机数为4,那么它大于数组长度,此时s[:index]虽然可以成功,但是s[index:]是要失败的,因此第二次边界的检查是有必要的。
  4. 你可能会说,index不是最大值为3吗?怎么可能是4呢?要知道编译器在编译的时候,并不知道index的最大值是3呢。
  5. 总结:
    • 当数组的长度和容量相等时,s[:index]成立能够保证s[index:]也成立,因为只要检查一次即可。
    1. 当数组的长度和容量不等时,s[:index]成立不能保证s[index:]也成立,因为要检查两次才可以。

案例四

  1. 由于数组是调用者传入的参数,所以编译器编译的时候无法得知数组的长度和容量是否相等,因此只能保险一点,两个都检查。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "math/rand"
)

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

func main()  {}
  1. 但是如果把两个表达式的顺序反过来,就只要做一次检查就行了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "math/rand"
)

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

func main()  {}

主动消除边界检查

  1. 虽然编译器已经非常努力去消除一些应该消除的边界检查,但难免会有一些遗漏。
  2. 这就需要”警民合作”,对于那些编译器还未考虑到的场景,但开发者又极力追求程序的运行效率的,可以使用一些小技巧给出一些暗示,告诉编译器哪些地方可以不用做边界检查。
  3. 比如下面这个示例,从代码的逻辑上来说,是完全没有必要做边界检查的,但是编译器并没有那么智能,实际上每个for循环,它都要做一次边界的检查,非常的浪费性能。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main


func f0(is []int, bs []byte) {
    if len(is) >= 256 {
        //is=is[:256]
        for _, n := range bs {
            // 每个循环都要边界检查,
            // 因为编译器并不知道is[n]这里的is的长度是否会超过byte大小
            _ = is[n] 
        }
    }
}

func main()  {}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
TEXT main.f0(SB) /mnt/hgfs/g/hello1/struct.go
func f0(is []int, bs []byte) {
  0x4552e0      4883ec50            SUBQ $0x50, SP
  0x4552e4      48896c2448          MOVQ BP, 0x48(SP)
  0x4552e9      488d6c2448          LEAQ 0x48(SP), BP
  0x4552ee      4889442458          MOVQ AX, 0x58(SP) # is.data
  0x4552f3      48895c2460          MOVQ BX, 0x60(SP) # is.len
  0x4552f8      48894c2468          MOVQ CX, 0x68(SP) # is.cap
  0x4552fd      48897c2470          MOVQ DI, 0x70(SP) # bs.data
  0x455302      4889742478          MOVQ SI, 0x78(SP) # bs.len
  0x455307      4c89842480000000    MOVQ R8, 0x80(SP) # bs.cap
    if len(is) >= 256 {
  0x45530f      48895c2428          MOVQ BX, 0x28(SP)   # BX=is.len
  0x455314      4881fb00010000      CMPQ $0x100, BX     # is.len 与 256比较
  0x45531b      7d02                JGE 0x45531f
  0x45531d      eb36                JMP 0x455355
        for _, n := range bs {
  0x45531f      488b542470          MOVQ 0x70(SP), DX   # DX=bs.data
  0x455324      488b5c2478          MOVQ 0x78(SP), BX   # BX=bs.len
  0x455329      488bb42480000000    MOVQ 0x80(SP), SI   # SI=bs.cap
  0x455331      4889542430          MOVQ DX, 0x30(SP)
  0x455336      48895c2438          MOVQ BX, 0x38(SP)
  0x45533b      4889742440          MOVQ SI, 0x40(SP)
  0x455340      48c744242000000000  MOVQ $0x0, 0x20(SP)
  0x455349      488b542438          MOVQ 0x38(SP), DX	# DX=bs.len
  0x45534e      4889542418          MOVQ DX, 0x18(SP)
  0x455353      eb0c                JMP 0x455361
}
  0x455355      eb00                JMP 0x455357
  0x455357      488b6c2448          MOVQ 0x48(SP), BP
  0x45535c      4883c450            ADDQ $0x50, SP
  0x455360      c3                  RET
        for _, n := range bs {
  0x455361      488b542420          MOVQ 0x20(SP), DX   # DX=0
  0x455366      4839542418          CMPQ DX, 0x18(SP)   # DX 与 bs.len 循环判断条件
  0x45536b      7f02                JG 0x45536f
  0x45536d      eb2e                JMP 0x45539d
  0x45536f      488b542430          MOVQ 0x30(SP), DX
  0x455374      4803542420          ADDQ 0x20(SP), DX
  0x455379      0fb602              MOVZX 0(DX), AX
  0x45537c      88442417            MOVB AL, 0x17(SP)
            _ = is[n]
  0x455380      488b4c2460          MOVQ 0x60(SP), CX
  0x455385      4839c1              CMPQ AX, CX     # 越界判断
  0x455388      7702                JA 0x45538c
  0x45538a      eb13                JMP 0x45539f
  0x45538c      eb00                JMP 0x45538e
        for _, n := range bs {
  0x45538e      488b542420          MOVQ 0x20(SP), DX
  0x455393      48ffc2              INCQ DX
  0x455396      4889542420          MOVQ DX, 0x20(SP)
  0x45539b      ebc4                JMP 0x455361
}
  0x45539d      ebb8                JMP 0x455357
            _ = is[n]
  0x45539f      90                  NOPL
  0x4553a0      e81bd3ffff          CALL runtime.panicIndex(SB)
  0x4553a5      90                  NOPL
  1. 可以试着在for循环前加上这么一句is = is[:256]来告诉编译器新is的长度为256,最大索引值为255,不会超过byte的最大值,因为is[n]从逻辑上来说是一定不会越界的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func f00(is []int, bs []byte) {
    if len(is) >= 256 {
        is = is[:256]
        for _, n := range bs {
            _ = is[n] // 不需要做边界检查
        }
    }
}
func main()  {}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
TEXT main.f00(SB) /mnt/hgfs/g/hello1/t1.go
func f00(is []int, bs []byte) {
  0x4552e0      4883ec50            SUBQ $0x50, SP
  0x4552e4      48896c2448          MOVQ BP, 0x48(SP)
  0x4552e9      488d6c2448          LEAQ 0x48(SP), BP
  0x4552ee      4889442458          MOVQ AX, 0x58(SP)
  0x4552f3      48895c2460          MOVQ BX, 0x60(SP)
  0x4552f8      48894c2468          MOVQ CX, 0x68(SP)
  0x4552fd      48897c2470          MOVQ DI, 0x70(SP)
  0x455302      4889742478          MOVQ SI, 0x78(SP)
  0x455307      4c89842480000000    MOVQ R8, 0x80(SP)
    if len(is) >= 256 {
  0x45530f      48895c2428          MOVQ BX, 0x28(SP)
  0x455314      4881fb00010000      CMPQ $0x100, BX     # 256 与 is.len 比较
  0x45531b      7d02                JGE 0x45531f
  0x45531d      eb54                JMP 0x455373
        is = is[:256]       // 消除边界检查
  0x45531f      488b542468          MOVQ 0x68(SP), DX   # DX=is.cap
  0x455324      4881fa00010000      CMPQ $0x100, DX     # 256 与 is.cap 比较
  0x45532b      7305                JAE 0x455332
  0x45532d      e993000000          JMP 0x4553c5
  0x455332      eb00                JMP 0x455334
  0x455334      48c744246000010000  MOVQ $0x100, 0x60(SP)   # is.len=256
        for _, n := range bs {
  0x45533d      488b542470          MOVQ 0x70(SP), DX   # DX=bs.data
  0x455342      488b5c2478          MOVQ 0x78(SP), BX   # BX=bs.len
  0x455347      488bb42480000000    MOVQ 0x80(SP), SI   # SI=bs.cap
  0x45534f      4889542430          MOVQ DX, 0x30(SP)
  0x455354      48895c2438          MOVQ BX, 0x38(SP)
  0x455359      4889742440          MOVQ SI, 0x40(SP)
  0x45535e      48c744242000000000  MOVQ $0x0, 0x20(SP)
  0x455367      488b542438          MOVQ 0x38(SP), DX   # DX=bs.len
  0x45536c      4889542418          MOVQ DX, 0x18(SP)
  0x455371      eb0c                JMP 0x45537f
}
  0x455373      eb00                JMP 0x455375
  0x455375      488b6c2448          MOVQ 0x48(SP), BP
  0x45537a      4883c450            ADDQ $0x50, SP
  0x45537e      c3                  RET
        for _, n := range bs {
  0x45537f      488b542420          MOVQ 0x20(SP), DX   # DX=0
  0x455384      4839542418          CMPQ DX, 0x18(SP)   # 0 与 bs.len 比较
  0x455389      7f02                JG 0x45538d
  0x45538b      eb2e                JMP 0x4553bb
  0x45538d      488b542430          MOVQ 0x30(SP), DX   # DX=bs.data
  0x455392      4803542420          ADDQ 0x20(SP), DX   # DX=0+DX
  0x455397      0fb602              MOVZX 0(DX), AX     # AX=*bs.data
  0x45539a      88442417            MOVB AL, 0x17(SP)
            _ = is[n]
  0x45539e      488b4c2460          MOVQ 0x60(SP), CX   # CX=256
  0x4553a3      4839c1              CMPQ AX, CX         # 越界判断
  0x4553a6      7702                JA 0x4553aa
  0x4553a8      eb13                JMP 0x4553bd
  0x4553aa      eb00                JMP 0x4553ac
        for _, n := range bs {
  0x4553ac      488b542420          MOVQ 0x20(SP), DX
  0x4553b1      48ffc2              INCQ DX
  0x4553b4      4889542420          MOVQ DX, 0x20(SP)
  0x4553b9      ebc4                JMP 0x45537f
}
  0x4553bb      ebb8                JMP 0x455375
            _ = is[n]
  0x4553bd      0f1f00              NOPL 0(AX)
  0x4553c0      e8fbd2ffff          CALL runtime.panicIndex(SB)
        is = is[:256]       // 消除边界检查
  0x4553c5      b900010000          MOVL $0x100, CX
  0x4553ca      e871d3ffff          CALL runtime.panicSliceAcap(SB)
  0x4553cf      90                  NOPL