Goの文字列結合のパフォーマンス

  • 134
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

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はこういうベンチマークがサッと書けるので良い