Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GoのBCEについて

Avatar for Tan1 Tan1
July 02, 2024
11

 GoのBCEについて

Avatar for Tan1

Tan1

July 02, 2024

Transcript

  1. とある Generics のコードにて // exp/slices/slices.go L159... func Delete[S ~[]E, E

    any](s S, i, j int) S { _ = s[i:j] // bounds check return append(s[:i], s[j:]...) } slice の対象範囲を削除するGenerics を利用したSlice 操作の一例 _ = s[i:j] によって何か影響があるのか?... 2
  2. BCE とは? SSA について SSA とは静的単一代入 (static simple assignment) の略称であり、

    コンパイラにおける中間表現の一つ Go 1.7 以降、標準のGo コンパイラにSSA が導入 対象のコードから変数の性質を簡単にすることにより 各種コンパイラ最適化を実現・改善することができる SSA は各変数への代入が一度のみ行われる式によって構成される (1 回だけ割り当てられる) 3
  3. SSA について y := 1 y = 2 x :=

    y // SSA y0 := 1 // 使われていない y1 := 2 x0 := y1 GOSSAFUNC=f1 go build main.go GOSSAFUNC を指定しビルドするとhtml ファイルが生成され、 対象関数のコンパイルにおける各フェーズの詳細が出力 4
  4. BCE とは? BCE(bounds checking elimination) はコンパイラ最適化の一種で 冗長な領域チェック(BC) を除去 SSA 導入に合わせてBCE

    も導入された BC はメモリー領域の確認を行いメモリ領域の不正アクセスを防ぐ役割 BC を無効化するとスライスなど値の領域に属さないメモリにアクセス できてしまう スライスや文字列の領域を超えてアクセスしようとするとpanic を起こ すことができる BC には index 用(a[i]) と slice 用(a[i:j]) の2 種類が存在 5
  5. func f1(s []int) { _ = s[0] // bounds check

    _ = s[1] // bounds check _ = s[2] // bounds check } func f2(s []int) { _ = s[2] // bounds check _ = s[1] // bounds check eliminated! _ = s[0] // bounds check eliminated! } func f3(s []int, index int) { _ = s[index] // bounds check _ = s[index] // bounds check eliminated! } func f4(a [5]int) { _ = a[4] // bounds check eliminated! } 6
  6. BCE とは? $ go run -gcflags="-d=ssa/check_bce/debug=1" main.go ./main.go:5: Found IsInBounds

    ./main.go:6: Found IsInBounds ./main.go:7: Found IsInBounds ./main.go:11: Found IsInBounds ./main.go:17: Found IsInBounds 7
  7. 実行速度どれくらいなのか package main import "testing" func BenchmarkBCE_f1(b *testing.B) { nums

    := make([]int, 0, 3) for i := 0; i < cap(nums); i++ { nums = append(nums, i) } for n := 0; n < b.N; n++ { f1(nums) } } func BenchmarkBCE_f2(b *testing.B) { nums := make([]int, 0, 3) for i := 0; i < cap(nums); i++ { nums = append(nums, i) } for n := 0; n < b.N; n++ { f2(nums) } } 8
  8. 実行速度どれくらいなのか goos: darwin goarch: amd64 pkg: sge_go_9_28 cpu: Intel(R) Core(TM)

    i9-9980HK CPU @ 2.40GHz BenchmarkBCE_f1 BenchmarkBCE_f1-16 1000000000 0.4530 ns/op 0 B/op 0 allocs/op BenchmarkBCE_f2 BenchmarkBCE_f2-16 1000000000 0.2805 ns/op 0 B/op 0 allocs/op PASS ok sge_go_9_28 1.460s 9
  9. 他にどんな場合にチェックが除外されるのか // bounds check eliminated! func f5(s []int) { for

    i := range s { _ = s[i] _ = s[i:len(s)] _ = s[:i+1] } } // bounds check eliminated! func f6(s []int) { for i := 0; i < len(s); i++ { _ = s[i] _ = s[i:len(s)] _ = s[:i+1] } } 10
  10. 他にどんな場合にチェックが除外されるのか // bounds check eliminated! func f7(s []int) { for

    i := len(s) - 1; i >= 0; i-- { _ = s[i] _ = s[i:len(s)] } } // bounds check eliminated! func f8(s []int) []int { s2 := make([]int, len(s)) for i := range s { s2[i] = -s[i] } return s2 } 11
  11. 他にどんな場合にチェックが除外されるのか // bounds check eliminated! func f9(s []int, index int)

    { if index >= 0 && index < len(s) { _ = s[index] _ = s[index:len(s)] } } // bounds check eliminated! func f10(s []int) { if len(s) > 2 { _, _, _ = s[0], s[1], s[2] } } 12
  12. slice 式での注意点 import "math/rand" func fa() { s := []int{0,

    1, 2, 3, 4, 5, 6} index := rand.Intn(7) _ = s[:index] // bounds check _ = s[index:] // bounds check eliminated! } func fb(s []int, i int) { _ = s[:i] // bounds check _ = s[i:] // 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] // bounds check _ = s[i:] // bounds check, not smart enough? } 14
  13. slice 式での注意点 slice 式の開始index が対象slice のlen よりも大きくなる可能性 があるためチェックが必要となる package main

    func main() { s := make([]int, 5, 10) // len(s0) == 5, cap(s0) == 10 index := 8 _ = s[:index] _ = s[index:] // panic } 15
  14. slice 式での注意点 len == cap の場合のみlen パラメータのチェック後は 同じ値でのindex パラメータの安全が保障されるためBCE が適応

    func fc() { s := []int{0, 1, 2, 3, 4, 5, 6} s = s[:4] i := rand.Intn(7) _ = s[:i] // bounds check _ = s[i:] // bounds check } func fc() { s := []int{0, 1, 2, 3, 4, 5, 6} s = s[:4:4] // <- 3 つ目のindex 指定でcap まで指定する i := rand.Intn(7) _ = s[:i] // bounds check _ = s[i:] // bounds check eliminated! } 16
  15. 実際にどんなところで活かせる? var masterData = []interface{}{"apple", 300, "banana", 500} type item

    struct { name string price int } func f1(data []interface{}) []*item { items := make([]*item, 0) for i := 0; i < len(data); i += 2 { items = append(items, &item{ name: data[i].(string), // bounds check price: data[i+1].(int), // bounds check }) } return items } func f2(data []interface{}) []*item { items := make([]*item, 0) for i := 0; i < len(data); i += 2 { target := data[i : i+2] // bounds check items = append(items, &item{ name: target[0].(string), price: target[1].(int), }) } return items } 17
  16. 実際にどんなところで活かせる? func BenchmarkBCE_f1(b *testing.B) { data := make([]interface{}, 0, len(masterData))

    for i := 0; i < len(masterData); i++ { data = append(data, masterData[i]) } for n := 0; n < b.N; n++ { f1(data) } } func BenchmarkBCE_f2(b *testing.B) { data := make([]interface{}, 0, len(masterData)) for i := 0; i < len(masterData); i++ { data = append(data, masterData[i]) } for n := 0; n < b.N; n++ { f2(data) } } 18
  17. 実際にどんなところで活かせる? goos: darwin goarch: amd64 pkg: sge_go_9_28 cpu: Intel(R) Core(TM)

    i9-9980HK CPU @ 2.40GHz BenchmarkBCE_f1-16 9707776 111.5 ns/op 72 B/op 4 allocs/op BenchmarkBCE_f1-16 10159305 108.4 ns/op 72 B/op 4 allocs/op BenchmarkBCE_f1-16 10313290 109.5 ns/op 72 B/op 4 allocs/op BenchmarkBCE_f2-16 10332660 105.0 ns/op 72 B/op 4 allocs/op BenchmarkBCE_f2-16 9810034 106.7 ns/op 72 B/op 4 allocs/op BenchmarkBCE_f2-16 10234861 108.2 ns/op 72 B/op 4 allocs/op 19
  18. とある Generics コードの検証 func Delete1[S ~[]E, E any](s S, i,

    j int) S { return append(s[:i], s[j:]...) } func Delete2[S ~[]E, E any](s S, i, j int) S { _ = s[i:j] return append(s[:i], s[j:]...) } func Benchmark_Delete1(b *testing.B) { nums := []int{0, 1, 2, 3, 4, 5} for n := 0; n < b.N; n++ { Delete1(nums, 1, 3) } } func Benchmark_Delete2(b *testing.B) { nums := []int{0, 1, 2, 3, 4, 5} for n := 0; n < b.N; n++ { Delete2(nums, 1, 3) } } 20
  19. とある Generics コードの検証 goos: darwin goarch: amd64 pkg: sge_go_9_28 cpu:

    Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz Benchmark_Delete1-16 279065300 4.124 ns/op 0 B/op 0 allocs/op Benchmark_Delete1-16 286361373 4.046 ns/op 0 B/op 0 allocs/op Benchmark_Delete1-16 284036828 4.084 ns/op 0 B/op 0 allocs/op Benchmark_Delete2-16 288069489 4.096 ns/op 0 B/op 0 allocs/op Benchmark_Delete2-16 288682585 4.054 ns/op 0 B/op 0 allocs/op Benchmark_Delete2-16 287124830 4.083 ns/op 0 B/op 0 allocs/op 大して変化がない... ? 21
  20. とある Generics コードの検証 func Delete1[S ~[]E, E any](s S, i,

    j int) S { return append(s[:i], s[j:]...) // bounds check *2 } func Delete2[S ~[]E, E any](s S, i, j int) S { _ = s[i:j] // bounds check return append(s[:i], s[j:]...) // bounds check } 結果的に2 回チェックが走っているので記述する必要なさそう 22
  21. BCE を意識したい部分 とにかく配列を多く操作(for など) するロジック 画像やゲームエンジンなどでの画面描画系では (Frame * height *

    weight) などのループが多く発生しそうなので 意識すると影響は大きそう (ebiten のロジックとか意識している部分あるかも?) 効率化の例 https://github.com/disintegration/imaging/pull/81 23
  22. まとめ BC を意識しなくても優秀なコンパイラが最適化してくれている ただし、index 参照やslice 式(slicing) を多用するような場所では大きく 影響が出るかも interface で抽象化している処理にてBC

    が多用される場合に Generics 化することでSort を効率化することができた事例 https://eli.thegreenplace.net/2022/faster-sorting-with-go-generics/ 24