go言語のBenchmarkTestメモ

【題材1】sliceのappend

参考: Bad Go: not sizing slices - The Startup - Medium

以下(大体)処理速度が遅い順番に記載

  1. BenchmarkTest1: 単純なappend
  2. BenchmarkTest2: 事前に追加するデータの個数分長さを確保してからappend
  3. BenchmarkTest3: 事前に追加するデータの個数分容量を確保してからappend
  4. BenchmarkTest4: 事前に追加するデータの個数分長さを確保してから要素を指定して代入
package main

import (
    "fmt"
    "testing"
)

var N = 1000000

func BenchmarkTest1(b *testing.B) {
    b.ResetTimer()
    list := []string{}
    for i := 0; i < N; i++ {
        list = append(list, fmt.Sprintf("%d", i))
    }
}

func BenchmarkTest2(b *testing.B) {
    b.ResetTimer()
    list := make([]string, N)
    for i := 0; i < N; i++ {
        list = append(list, fmt.Sprintf("%d", i))
    }
}

func BenchmarkTest3(b *testing.B) {
    b.ResetTimer()
    list := make([]string, 0, N)
    for i := 0; i < N; i++ {
        list = append(list, fmt.Sprintf("%d", i))
    }
}

func BenchmarkTest4(b *testing.B) {
    b.ResetTimer()
    list := make([]string, N)
    for i := 0; i < N; i++ {
        list[i] = fmt.Sprintf("%d", i)
    }
}

結果

Ubuntuでやったとき

$go test -bench . -benchmem -count=4
goos: linux
goarch: amd64

BenchmarkTest1  1000000000               0.269 ns/op           0 B/op          0 allocs/op
BenchmarkTest1  1000000000               0.277 ns/op           0 B/op          0 allocs/op
BenchmarkTest1  1000000000               0.294 ns/op           0 B/op          0 allocs/op
BenchmarkTest1  1000000000               0.297 ns/op           0 B/op          0 allocs/op
BenchmarkTest2  1000000000               0.240 ns/op           0 B/op          0 allocs/op
BenchmarkTest2  1000000000               0.272 ns/op           0 B/op          0 allocs/op
BenchmarkTest2  1000000000               0.251 ns/op           0 B/op          0 allocs/op
BenchmarkTest2  1000000000               0.266 ns/op           0 B/op          0 allocs/op
BenchmarkTest3  1000000000               0.129 ns/op           0 B/op          0 allocs/op
BenchmarkTest3  1000000000               0.101 ns/op           0 B/op          0 allocs/op
BenchmarkTest3  1000000000               0.101 ns/op           0 B/op          0 allocs/op
BenchmarkTest3  1000000000               0.0998 ns/op          0 B/op          0 allocs/op
BenchmarkTest4  1000000000               0.0965 ns/op          0 B/op          0 allocs/op
BenchmarkTest4  1000000000               0.103 ns/op           0 B/op          0 allocs/op
BenchmarkTest4  1000000000               0.102 ns/op           0 B/op          0 allocs/op
BenchmarkTest4  1000000000               0.101 ns/op           0 B/op          0 allocs/op
PASS
ok           43.708s

Macでやったとき

----result----
$go test -bench . -benchmem -count=4
goos: darwin
goarch: amd64

BenchmarkTest1-8        1000000000               0.159 ns/op           0 B/op          0 allocs/op
BenchmarkTest1-8        1000000000               0.174 ns/op           0 B/op          0 allocs/op
BenchmarkTest1-8        1000000000               0.174 ns/op           0 B/op          0 allocs/op
BenchmarkTest1-8        1000000000               0.178 ns/op           0 B/op          0 allocs/op
BenchmarkTest2-8        1000000000               0.195 ns/op           0 B/op          0 allocs/op
BenchmarkTest2-8        1000000000               0.197 ns/op           0 B/op          0 allocs/op
BenchmarkTest2-8        1000000000               0.198 ns/op           0 B/op          0 allocs/op
BenchmarkTest2-8        1000000000               0.195 ns/op           0 B/op          0 allocs/op
BenchmarkTest3-8        1000000000               0.108 ns/op           0 B/op          0 allocs/op
BenchmarkTest3-8        1000000000               0.102 ns/op           0 B/op          0 allocs/op
BenchmarkTest3-8        1000000000               0.108 ns/op           0 B/op          0 allocs/op
BenchmarkTest3-8        1000000000               0.102 ns/op           0 B/op          0 allocs/op
BenchmarkTest4-8        1000000000               0.104 ns/op           0 B/op          0 allocs/op
BenchmarkTest4-8        1000000000               0.105 ns/op           0 B/op          0 allocs/op
BenchmarkTest4-8        1000000000               0.104 ns/op           0 B/op          0 allocs/op
BenchmarkTest4-8        1000000000               0.102 ns/op           0 B/op          0 allocs/op
PASS

【題材2】文字列の二次元Sliceをinterfaceの二次元Sliceに変換する

文字列の二次元Sliceをinterfaceの二次元Sliceにする

やっていることは上の話と同じ部分が多い

性能比較用の関数定義

以下のような関数を用意して性能を比較する

  • convertStringSlicesToInterfaceSlices1
    • なんの工夫もないappend
  • convertStringSlicesToInterfaceSlices2
    • 事前にmake(interface{}, 0, len(ss))で格納するスライスの長さ分容量を確保してからappend
  • convertStringSlicesToInterfaceSlices3
    • 事前にmake(interface{}, len(ss))で格納するスライスの長さ分のスライスを用意して、要素を指定して格納
  • convertStringSlicesToInterfaceSlices4
    • string から interfaceのの変換部分をreflectで行う関数interfaceSliceを使うように、convertStringSlicesToInterfaceSlices3を一部書き換え
    • reflectは遅いと聞いているので本当か確認する
    • 参考:go - Type converting slices of interfaces - Stack Overflow
func convertStringSlicesToInterfaceSlices1(sss [][]string) [][]interface{} {
    var iss [][]interface{}
    for _, ss := range sss {
        var is []interface{}
        for _, s := range ss {
            is = append(is, s)
        }
        iss = append(iss, is)
    }
    return iss
}

func convertStringSlicesToInterfaceSlices2(sss [][]string) [][]interface{} {
    iss := make([][]interface{}, 0, len(sss))
    for _, ss := range sss {
        is := make([]interface{}, 0, len(ss))
        for _, s := range ss {
            is = append(is, s)
        }
        iss = append(iss, is)
    }
    return iss
}

func convertStringSlicesToInterfaceSlices3(sss [][]string) [][]interface{} {
    iss := make([][]interface{}, len(sss))
    for i, ss := range sss {
        is := make([]interface{}, len(ss))
        for j, s := range ss {
            is[j] = s
        }
        iss[i] = is
    }
    return iss
}

func convertStringSlicesToInterfaceSlices4(sss [][]string) [][]interface{} {
    iss := make([][]interface{}, len(sss))
    for i, ss := range sss {
        iss[i] = interfaceSlice(ss)
    }
    return iss
}

func interfaceSlice(slice interface{}) []interface{} {
    s := reflect.ValueOf(slice)
    if s.Kind() != reflect.Slice {
        panic("interfaceSlice() given a non-slice type")
    }

    ret := make([]interface{}, s.Len())
    for i := 0; i < s.Len(); i++ {
        ret[i] = s.Index(i).Interface()
    }
    return ret
}

関数のテスト

念のため、全部の関数の機能がすべて想定通りかテストをしておく

以下では、makeStringSlicesで試験データを用意して、それを用意した関数に与えて、makeInterfaceSlicesと同じかどうかを比較する

  • ちなみにSssはStringSlices、IssはInterfaceSlicesの略
func makeStringSlices(n, m int) [][]string {
    var ress [][]string
    for i := 0; i < n; i++ {
        var res []string
        for j := 0; j < m; j++ {
            res = append(res, fmt.Sprintf("%d", j))
        }
        ress = append(ress, res)
    }
    return ress
}

func makeInterfaceSlices(n, m int) [][]interface{} {
    var ress [][]interface{}
    for i := 0; i < n; i++ {
        var res []interface{}
        for j := 0; j < m; j++ {
            res = append(res, fmt.Sprintf("%d", j))
        }
        ress = append(ress, res)
    }
    return ress
}

func TestConvertStringSlicesToInterfaceSlices(t *testing.T) {
    inputSss := makeStringSlices(3, 3)
    wantIss := makeInterfaceSlices(3, 3)
    t.Log("inputSss:", inputSss)
    t.Log("wantIss:", wantIss)
    if !reflect.DeepEqual(convertStringSlicesToInterfaceSlices1(inputSss), wantIss) {
        t.Error("failed to convertStringSlicesToInterfaceSlices1")
    }
    if !reflect.DeepEqual(convertStringSlicesToInterfaceSlices2(inputSss), wantIss) {
        t.Error("failed to convertStringSlicesToInterfaceSlices2")
    }
    if !reflect.DeepEqual(convertStringSlicesToInterfaceSlices3(inputSss), wantIss) {
        t.Error("failed to convertStringSlicesToInterfaceSlices3")
    }
    if !reflect.DeepEqual(convertStringSlicesToInterfaceSlices4(inputSss), wantIss) {
        t.Error("failed to convertStringSlicesToInterfaceSlices4")
    }
}

test実行結果 - inputSssとwantIssが同じことが確認できた

$go test -v sample/sample_test.go --run TestConvertStringSlicesToInterfaceSlices              
=== RUN   TestConvertStringSlicesToInterfaceSlices
--- PASS: TestConvertStringSlicesToInterfaceSlices (0.00s)
    sample_test.go:93: inputSss: [[0 1 2] [0 1 2] [0 1 2]]
    sample_test.go:94: wantIss: [[0 1 2] [0 1 2] [0 1 2]]
PASS
ok      command-line-arguments  0.001s

Benchmarkを計測

各関数の働きがすべて同じだということがわかったので、性能を確認する

以下で、var result interface{}を定義して、result = resと代入しているのには理由があって、for文でループをいっぱい回しても、forの外に影響を及ぼさないとコンパイラがfor文の中身を無視してしまってBenchmarkが想定通りに測れないという話を聞いたのでそれを参考にしている

参考:How to write benchmarks in Go | Dave Cheney

var N = 10000
var result [][]interface{}

func BenchmarkTestConvertSlices1(b *testing.B) {
    inputSss := makeStringSlices(10, 10)
    b.ResetTimer()
    var res [][]interface{}
    for i := 0; i < N; i++ {
        res = convertStringSlicesToInterfaceSlices1(inputSss)
    }
    result = res
}

func BenchmarkTestConvertSlices2(b *testing.B) {
    inputSss := makeStringSlices(10, 10)
    b.ResetTimer()
    var res [][]interface{}
    for i := 0; i < N; i++ {
        res = convertStringSlicesToInterfaceSlices2(inputSss)
    }
    result = res
}

func BenchmarkTestConvertSlices3(b *testing.B) {
    inputSss := makeStringSlices(10, 10)
    b.ResetTimer()
    var res [][]interface{}
    for i := 0; i < N; i++ {
        res = convertStringSlicesToInterfaceSlices3(inputSss)
    }
    result = res
}

func BenchmarkTestConvertSlices4(b *testing.B) {
    inputSss := makeStringSlices(10, 10)
    b.ResetTimer()
    var res [][]interface{}
    for i := 0; i < N; i++ {
        res = convertStringSlicesToInterfaceSlices4(inputSss)
    }
    result = res
}

count=3で各Benchmarkを3回ずつ実行した結果は以下の通り

[~/go/src/github.com/ludwig125_gosample] $go test -benchmem -run=^$ github.com/ludwig125_gosample/convertSlice -bench . --count=3
goos: linux
goarch: amd64
pkg: github.com/ludwig125_gosample/convertSlice
BenchmarkTestConvertSlices1     1000000000               0.0644 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices1     1000000000               0.0650 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices1     1000000000               0.0720 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices2     1000000000               0.0385 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices2     1000000000               0.0363 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices2     1000000000               0.0358 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices3     1000000000               0.0342 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices3     1000000000               0.0349 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices3     1000000000               0.0435 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices4     1000000000               0.0629 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices4     1000000000               0.0662 ns/op          0 B/op          0 allocs/op
BenchmarkTestConvertSlices4     1000000000               0.0655 ns/op          0 B/op          0 allocs/op
PASS
ok      github.com/ludwig125_gosample/convertSlice      5.747s
[~/go/src/github.com/ludwig125_gosample] $

まとめ

  • sliceの容量を確保してからappendするBenchmarkTestConvertSlices2か、長さを確保してから要素に代入するBenchmarkTestConvertSlices3が速いことが確認できた
  • reflectは遅いので使わないほうがいいと再認識した

  • 実験につかった関数名が長くて後悔した