Runner in the High

技術のことをかくこころみ

makeのlengthとcapacityを間違えると予想外のアロケーションが発生することがある&bytes.Bufferの処理速度について

izumisy.work

上記の記事をstackoverflowで質問したら回答がついた。

stackoverflow.com

結論から言うと自分はmakeの使い方を間違えていて、makeの第3引数にあたるcapacityを省略してスライスを作成するとそこには空データが埋まってしまうとのこと。

なので、もともとの make([]byte, ALLOC_SIZE) だと、最初からALLOC_SIZE分のデータが詰まっている状態から始まるためappendする度に毎回拡張のアロケーションが発生していた。正しくは make([]byte, 0, ALLOC_SIZE) として宣言するべきで、こうすることでアロケーションの回数は bytes.Buffer を使ったときと同じになる。

この辺の話は以下の記事も詳しい。

note.com

bytes.Bufferとmakeの処理速度

bytes.Buffer も内部的にはmakeしているので、 理論上は bytes.Buffer よりもただ単純にmakeするだけのほうが速い。実際にそういうベンチマークの結果もある。

github.com

しかし、appendでデータを詰め始めたときの処理速度には違いが出る。

テストコードはこれ。

package app

import (
    "bytes"
    "testing"
)

const ALLOC_SIZE = 1024 * 1024 * 1024

func BenchmarkFunc1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := make([]byte, 0, ALLOC_SIZE)
        fill(v, '1', 0, ALLOC_SIZE)
    }
}

func BenchmarkFunc2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := new(bytes.Buffer)
        v.Grow(ALLOC_SIZE)
        fill(v.Bytes(), '2', 0, ALLOC_SIZE)
    }
}

func fill(slice []byte, val byte, start, end int) {
    for i := start; i < end; i++ {
        slice = append(slice, val)
    }
}

この結果は以下になる

at 13:32:12 ❯ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: app
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFunc1-8               1        1489858750 ns/op        1073743744 B/op        4 allocs/op
BenchmarkFunc2-8               2         930927369 ns/op        1073742880 B/op        3 allocs/op
PASS
ok      app     4.395s

1.5倍近く処理速度の差がある。なお、Growしない場合にはbytes.Bufferのほうが遅い。

makeとbytes.Bufferでは少なくともアロケーションの回数は同等だがアロケーションの速度はmakeのほうが確実に速い。一方、appendしてデータを詰める処理をし始めるとmakeよりもbytes.BufferしてGrowしたほうが処理速度的に優位になるということが分かった。

ここまでの処理をまとめたベンチマークのテストの結果を整理すると以下になる。

処理 確保 確保 + 書き込み
make 2976935 ns/op 66047937 ns/op
bytes.Buffer 0.3202 ns/op 85604838 ns/op
bytes.Buffer (Grow) 2950193 ns/op 44761869 ns/op

bytes.BufferはGrowしなければ確保の速度は最速だが、データを書き込み始めるととんでもなく遅い。メモリだけ確保しておいてデータは書き込まないということは無いはずなので、あまり意味のない速度な気がする。

少なくともこの中で一番無難なのはbytes.BufferでGrow使うことなのかな。