LoginSignup
240
180

More than 5 years have passed since last update.

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

Posted at

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
240
180