グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



【Go】ベンチマーク機能を使って高速でメモリ使用量の少ないプログラムを書こう

先日、メモリ使用量をなるべく抑えたい処理を実装するにあたってGoのベンチマーク機能を使用しました。
今回はそのベンチマーク機能について記事を書いていこうと思います。

ベンチマークのコードの書き方

実際にベンチマーク機能で使うコードを書いてみます。

go
func BenchmarkExample(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // ベンチマークを実行したい処理をループ内に書く
        var s []int
        s = append(s, 0)    
    }
}

テストファイル内にあるBenchmarkから始まる関数をGoはベンチマークとして扱ってくれます。関数の引数はb *testing.Bです。
関数の中に書かれているforループb.Nによって、計測結果が妥当な数値になるまでループしてくれます。このループの中に実行速度などを計測したい処理を書きます。

ベンチマークの実行

それではベンチマークを実行してみます。以下のようなコマンドを使用します。

go test -bench=. -benchmem

-benchフラグをつけることでベンチマークを実行できます。すべてのベンチマークを実行したい場合は-bench=.もしくは-bench .と入力します。また、今回はメモリの情報も取得したいので、-benchmemフラグもつけました。

実行結果の出力は以下のようになります。

BenchmarkExample-20     80786863                12.75 ns/op            8 B/op          1 allocs/op

出力内容は、左から順に以下の通りです。

  • ベンチマーク関数名
  • 試行回数
  • 1回の実行にかかる時間
  • 1回の実行で割り当てられたメモリのバイトサイズ
  • 1回の実行で発生したアロケーションの回数

ベンチマーク使用時の注意点

大変便利なベンチマーク機能ですが、使い方には注意するべき点があります。以下のコードを見てください。

func AppendSlice() {
    var s []int
    for i := range 1000 {
        s = append(s, i)
    }
}

func NoAppendSlice() {
    s := make([]int, 1000)
    for i := range 1000 {
        s[i] = i
    }
}

func BenchmarkAppendSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        AppendSlice()
    }
}

func BenchmarkNoAppendSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        NoAppendSlice()
    }
}

スライスをその都度appendする関数と、最初から1000の容量のスライスを用意する関数をベンチマークで処理を計測してみます。後者の方がメモリ使用量が抑えられている結果が出るのではと期待して実行します。

ベンチマークテストの結果は以下のようになりました。

BenchmarkAppendSlice-20           293276              4124 ns/op           25208 B/op         12 allocs/op
BenchmarkNoAppendSlice-20        3634183               343.6 ns/op             0 B/op          0 allocs/op

処理時間はやはり後者の方が短いことがわかります。しかし、メモリ系の結果を見てみると、メモリが全くアロケーションされていないような結果になっています。
これは、コンパイラが処理の最適化を頑張ってくれている影響で、そもそもスライスが用意されずにベンチマークテストの処理が終了している可能性があります。Go側でパフォーマンスを良くしてくれようとしてくれた結果、意図したようにベンチマークが計測できなくなってしまっています。

そのため、ベンチマークテストを実施する際には、少し工夫が必要です。以下のようにコードを変更することで、後者もメモリを使用した計測結果を出力してくれるようになります。

var ss []int // パッケージレベルの変数を用意

func AppendSlice() {
    var s []int
    for i := range 1000 {
        s = append(s, i)
    }
    ss = s // パッケージレベルの変数に代入
}

func NoAppendSlice() {
    s := make([]int, 1000)
    for i := range 1000 {
        s[i] = i
    }
    ss = s // パッケージレベルの変数に代入
}


func BenchmarkAppendSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        AppendSlice()
    }
}

func BenchmarkNoAppendSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        NoAppendSlice()
    }
}

実行結果:

BenchmarkAppendSlice-20           272253              4180 ns/op           25208 B/op         12 allocs/op
BenchmarkNoAppendSlice-20         772209              1580 ns/op            8192 B/op          1 allocs/op

パッケージレベルの変数に操作したスライスを代入すると、メモリがちゃんとアロケーションされている結果に変わりました。やはり最初から1000の容量のスライスを用意する方がメモリ使用量を抑えられていますね。

このようにコンパイラの影響なども考慮してコードを書く必要があるので注意しましょう。

さいごに

今回はGoの標準機能であるベンチマークについて紹介しました。
計測する際にはコンパイラの影響にも注意してコードを書く必要がありますが、とても便利な機能だと思いました。これからもこの機能を活用して軽く速いコードが書けるよう努めていきたいです。

この記事を書いた人

wanderlust
wanderlust事業開発部 web application engineer
これまで農業、士業と経験し、まったく異業種のエンジニアとしてアーティスに入社。
現在は事業開発部でバックエンドエンジニアとして仕事に従事。可読性の高いコードが書けるよう日々勉強中。趣味は一人旅。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします