Goで文字列結合をする時、通常の+=は遅いから[]byteをappendした方が高速という話があったので、実際にどの程度の差が出るのか検証してみた。
テストケース
以下のような9文字*10要素の文字列の配列要素を","で結合し、最後に","を追記するコードを実装した。得たい出力はstringなので、[]byteやbytes.Bufferを使う場合、最後にstringへのキャストを実行した。
var m = [...]string{
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
}
使用したコードはこちら:Golang string join benchmark
実装
+=演算子のループ
ナイーブな実装。遅い。
func BenchmarkAppendOperator_(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 string
for _, v := range m {
m2 += m2 + "," + v
}
m2 += ","
}
}
// BenchmarkAppendOperator 1000000 3043 ns/op 3808 B/op 8 allocs/op
結果:3043ns/op
strings.Join関数
strings.Joinを使った方法。
func BenchmarkStringsJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(m[:], ",") + ","
}
}
// BenchmarkStringsJoin 5000000 413 ns/op 240 B/op 3 allocs/op
結果:413ns/op
一度の代入文で+演算子で全要素を結合
一度の代入文で全ての要素を結合するようハードコーディングする。strings.joinよりもアロケーションが少なく高速
func BenchmarkHardCoding(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = m[0] + "," + m[1] + "," + m[2] + "," + m[3] + "," + m[4] + "," + m[5] + "," + m[6]
}
}
// BenchmarkHardCoding 10000000 216 ns/op 80 B/op 1 allocs/op
結果:216ns/op
[]byte
var m2 []byte
で宣言した[]byteにappend()を繰り返す。strings.Joinよりも遅くなっている。
func BenchmarkByteArray(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 []byte
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
// BenchmarkByteArray______ 5000000 613 ns/op 320 B/op 5 allocs/op
結果:613ns/op
キャパシティ指定付き[]byte
var m2 = make([]byte, 0, 100)
で[]byteに100byteのキャパシティを確保したところ、アロケーションが少なくなり、この方法が一番高速だった。最後のstringへのキャストを除くとアロケーションは0になる。
func BenchmarkCapByteArray___(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = make([]byte, 0, 100)
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
// BenchmarkCapByteArray___ 10000000 171 ns/op 80 B/op 1 allocs/op
結果:171ns/op
bytes.Buffer
bytes.Buffer
を使ってみる
func BenchmarkBytesBuffer____(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 bytes.Buffer
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
// BenchmarkBytesBuffer____ 1000000 1074 ns/op 449 B/op 10 allocs/op
(結果:1074 ns/op)
キャパシティ指定付き bytes.Buffer
NewBuffer
にキャパシティ指定した[]byteを渡してみる。
func BenchmarkCapBytesBuffer_(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
// BenchmarkCapBytesBuffer_ 2000000 956 ns/op 419 B/op 10 allocs/op
(結果:956ns/op)
キャパシティ指定付き bytes.Buffer + WriteString
Buffer.Write
のかわりにBuffer.WriteString
というメソッドを使ってみたところ、Buffer.Write
よりも速くなった。
func BenchmarkCapBytesBuffer2(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.WriteString(v)
m2.WriteString(",")
}
_ = m2.String()
}
}
// BenchmarkCapBytesBuffer2 5000000 588 ns/op 307 B/op 3 allocs/op
結果:588ns/op
結果一覧
BenchmarkAppendOperator_ 1000000 3043 ns/op 3808 B/op 8 allocs/op
BenchmarkStringsJoin____ 5000000 413 ns/op 240 B/op 3 allocs/op
BenchmarkHardCoding_____ 10000000 216 ns/op 80 B/op 1 allocs/op
BenchmarkByteArray______ 5000000 613 ns/op 320 B/op 5 allocs/op
BenchmarkCapByteArray___ 10000000 171 ns/op 80 B/op 1 allocs/op
BenchmarkBytesBuffer____ 1000000 1074 ns/op 449 B/op 10 allocs/op
BenchmarkCapBytesBuffer_ 2000000 956 ns/op 419 B/op 10 allocs/op
BenchmarkCapBytesBuffer2 5000000 588 ns/op 307 B/op 3 allocs/op
知見
大まかに各手法での処理時間を比べると、このような感じ。
[]byte < ハードコーディング(200ns) < strings.Join(400ns) < bytes.Buffer(600ns) < +=演算子ループ(2900ns)
- +=演算子ループはやはり一桁遅い。
- ただし文字列の+演算子は特に遅いと言う事は無いので、s := a + b + c のように、一度の代入で複数の結合を行うのは十分に高速
- bytes.Bufferより[]byteの方が高速
- makeで十分なキャパシティを確保してalloc回数を下げるのが重要
- bytes.Bufferでstringを結合する場合は、Write()よりWriteString()が高速
- Goはこういうベンチマークがサッと書けるので良い