上記の記事をstackoverflowで質問したら回答がついた。
結論から言うと自分はmakeの使い方を間違えていて、makeの第3引数にあたるcapacityを省略してスライスを作成するとそこには空データが埋まってしまうとのこと。
なので、もともとの make([]byte, ALLOC_SIZE)
だと最初からALLOC_SIZE分のデータが詰まっている状態から始まるため、そこにappendされデータが増えると再び拡張のアロケーションが発生していた。正しくは make([]byte, 0, ALLOC_SIZE)
として宣言するべきで、こうすることでアロケーションの回数は bytes.Buffer
を使ったときと同じになる。
この辺の話は以下の記事も詳しい。
bytes.Bufferとmakeのアロケーション処理速度
bytes.Buffer
も内部的にはmakeしているので、 理論上は bytes.Buffer
よりもただ単純にmakeするだけのほうが速い。実際にそういうベンチマークの結果もある。
しかし、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使うことなのかな。