Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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はこういうベンチマークがサッと書けるので良い
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした