前言
在這篇文章中碰巧看到了Go
邊界檢查消除相關的討論. 我也藉此簡單聊聊.
有這樣一段代碼, 非常簡單, 就是一段求向量點積的程序:
func sum(a, b []int) int {
if len(a) != len(b) {
panic("must be same len")
}
ret := 0
for i := 0; i < len(a); i++ {
ret += a[i] * b[i]
}
return ret
}
根據之前CPU 流水線的原理, 將其在數組內部展開可以提高循環計算效率:
package main
func sum(a, b []int) int {
if len(a) != len(b) {
panic("must be same len")
}
ret := 0
for i := 0; i < len(a); i += 4 {
s1 := a[i] * b[i]
s2 := a[i+1] * b[i+1]
s3 := a[i+2] * b[i+2]
s4 := a[i+3] * b[i+3]
ret += s1 + s2 + s3 + s4
}
return ret
}
到這裏, 就要引出Go
邊界檢查的概念了. 我們都知道, 在數組訪問越界的時候會觸發panic
, 這個其實是編譯期在編譯期間額外添加邊界檢查代碼實現的. 可以給go build
命令添加-gcflags='-d=ssa/check_bce'
參數來查看哪些地方觸發了邊界檢查:
我們可以理解爲, 上面的程序在編譯後是這樣的:
func sum(a, b []int) int {
if len(a) != len(b) {
panic("must be same len")
}
ret := 0
for i := 0; i < len(a); i += 4 {
if i >= cap(a) || i >= cap(b) {
panic("out of bounds")
}
s1 := a[i] * b[i]
if i+1 >= cap(a) || i+1 >= cap(b) {
panic("out of bounds")
}
s2 := a[i+1] * b[i+1]
if i+2 >= cap(a) || i+2 >= cap(b) {
panic("out of bounds")
}
s3 := a[i+2] * b[i+2]
if i+3 >= cap(a) || i+3 >= cap(b) {
panic("out of bounds")
}
s4 := a[i+3] * b[i+3]
ret += s1 + s2 + s3 + s4
}
return ret
}
在每次數組訪問前都會進行邊界檢查.
而如果我們將其改造成這樣, 就只需要2次邊界檢查.
func sum(a, b []int) int {
if len(a) != len(b) {
panic("must be same len")
}
ret := 0
for i := 0; i < len(a); i += 4 {
aTmp := a[i : i+4] // Found IsSliceInBounds
bTmp := b[i : i+4] // Found IsSliceInBounds
s1 := aTmp[0] * bTmp[0]
s2 := aTmp[1] * bTmp[1]
s3 := aTmp[2] * bTmp[2]
s4 := aTmp[3] * bTmp[3]
ret += s1 + s2 + s3 + s4
}
return ret
}
場景
簡單列一些邊界檢查的場景, 僅供參考:
func check(a []int, b [5]int, i int) {
// 重複訪問
_ = a[2] // Found IsSliceInBounds
_ = a[2] // 重複訪問, 消除邊界檢查
// 長度判斷
if 3 < len(a) {
_ = a[3] // 提前判斷長度, 無需邊界檢查
}
// 常量數組
_ = b[4] // 固定長度數組, 無需邊界檢查
// 提前邊界檢查
_ = a[5] // Found IsSliceInBounds
_ = a[4] // 因爲上邊檢查過5, 所以這裏無需邊界檢查
_ = a[3]
}
如果足夠自行, 我們也可以在編譯的時候添加參數-gcflags=-B
來禁用邊界檢查.
在這篇文章中有一些其他場景供參考.
OK, 這裏拋磚引玉, 簡單說一下邊界檢查這玩意, 感興趣的也可以查看編譯後的彙編代碼來了解具體是如何進行邊界檢查的.