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

Goで文字列結合をする時、通常の+=は遅いから[]byteをappendした方が高速という話があったので、実際にどの程度の差が出るのか検証してみた。

- [Goでは文字列連結はコストの高い操作 - Qiita](http://qiita.com/ruiu/items/2bb83b29baeae2433a79)

# テストケース
以下のような9文字*10要素の文字列の配列要素を","で結合し、最後に","を追記するコードを実装した。得たい出力はstringなので、[]byteやbytes.Bufferを使う場合、最後にstringへのキャストを実行した。

```go
var m = [...]string{
	"AAAAAAAAA",
	"AAAAAAAAA",
	"AAAAAAAAA",
	"AAAAAAAAA",
	"AAAAAAAAA",
	"AAAAAAAAA",
	"AAAAAAAAA",
}
```

使用したコードはこちら：[Golang string join benchmark](https://gist.github.com/matope/89ceb77d39e3c0407d67)

# 実装
## +=演算子のループ
ナイーブな実装。遅い。

```go
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を使った方法。

```go
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よりもアロケーションが少なく高速

```go
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よりも遅くなっている。

```go
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になる。

```go

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`` を使ってみる

```go
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を渡してみる。

```go
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``よりも速くなった。

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