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使うことなのかな。