はじめに
2018年2月にリリース予定のGo1.10では、strings.Builder
という型が(CL74931)が入ります。
このstrings.Builder
を用いると、効率良く文字列を生成することができます。
この記事では、まずstrings.Builder
の使い方を説明し、次に同様の使い方ができるbytes.Buffer
とベンチマークを取ってパフォーマンスを比較してみます。
そしてさらに、strings.Builder
の実装をソースコードから読み解き、そこで使われているテクニックを解説します。
使い方
まずは使い方をみていきましょう。
strings.Builder
はゼロ値で扱える型です。そのため、使用するのに初期化は特にいらず、次のように変数を定義するだけで使用することができます。
var b strings.Builder
strings.Builder
はio.Writer
インタフェースを実装しているため、次のようにfmt.Fprint
などの関数で利用できます。
fmt.Fprint(&b, "hello")
文字列として取り出すには、次のようにString
メソッドを呼び出します。
var s string = b.String()
ベンチマーク
さて、bytes.Buffer
も似たような使い方ができますが、パフォーマンス的にはどちらが有利なのでしょうか?ベンチマークを取ってみようと思います。
ベンチマークに使用するコードは次のとおりです。
package main
import (
"bytes"
"fmt"
"strings"
"testing"
)
func stringsBuilder(b *testing.B, n int, grow bool) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
b.StopTimer()
if grow {
builder.Grow(n * 4)
}
b.StartTimer()
for j := 0; j < n; j++ {
fmt.Fprint(&builder, "TEST")
}
_ = builder.String()
}
}
func bytesBuffer(b *testing.B, n int, grow bool) {
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
b.StopTimer()
if grow {
buf.Grow(n * 4)
}
b.StartTimer()
for j := 0; j < n; j++ {
fmt.Fprint(&buf, "TEST")
}
_ = buf.String()
}
}
func BenchmarkStringsBuilderWithoutGrow(b *testing.B) {
for _, n := range []int{10, 50, 100, 200, 500} {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
stringsBuilder(b, n, false)
})
}
}
func BenchmarkStringsBuilderWithGrow(b *testing.B) {
for _, n := range []int{10, 50, 100, 200, 500} {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
stringsBuilder(b, n, true)
})
}
}
func BenchmarkBytesBufferWithoutGrow(b *testing.B) {
for _, n := range []int{10, 50, 100, 200, 500} {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
bytesBuffer(b, n, false)
})
}
}
func BenchmarkBytesBufferWithGrow(b *testing.B) {
for _, n := range []int{10, 50, 100, 200, 500} {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
bytesBuffer(b, n, true)
})
}
}
strings.Builder
とbytes.Buffer
を使った場合と、さらにGrow
メソッドを使って、あらかじめ内部のバッファを確保していた場合と何もしない場合で比べてみました。
結果は次のようになりました。
% go version
go version devel +a99deed39b Mon Jan 8 18:06:27 2018 +0000 darwin/amd64
% go test -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkStringsBuilderWithoutGrow/10-4 500000 2796 ns/op 160 B/op 5 allocs/op
BenchmarkStringsBuilderWithoutGrow/50-4 200000 6942 ns/op 544 B/op 7 allocs/op
BenchmarkStringsBuilderWithoutGrow/100-4 200000 10813 ns/op 1056 B/op 8 allocs/op
BenchmarkStringsBuilderWithoutGrow/200-4 100000 18351 ns/op 2080 B/op 9 allocs/op
BenchmarkStringsBuilderWithoutGrow/500-4 30000 39791 ns/op 7457 B/op 12 allocs/op
BenchmarkStringsBuilderWithGrow/10-4 1000000 1898 ns/op 32 B/op 1 allocs/op
BenchmarkStringsBuilderWithGrow/50-4 300000 5387 ns/op 32 B/op 1 allocs/op
BenchmarkStringsBuilderWithGrow/100-4 200000 8699 ns/op 32 B/op 1 allocs/op
BenchmarkStringsBuilderWithGrow/200-4 100000 15439 ns/op 32 B/op 1 allocs/op
BenchmarkStringsBuilderWithGrow/500-4 50000 35775 ns/op 32 B/op 1 allocs/op
BenchmarkBytesBufferWithoutGrow/10-4 1000000 2164 ns/op 160 B/op 2 allocs/op
BenchmarkBytesBufferWithoutGrow/50-4 200000 6242 ns/op 752 B/op 4 allocs/op
BenchmarkBytesBufferWithoutGrow/100-4 200000 10146 ns/op 1536 B/op 5 allocs/op
BenchmarkBytesBufferWithoutGrow/200-4 100000 17278 ns/op 3168 B/op 6 allocs/op
BenchmarkBytesBufferWithoutGrow/500-4 30000 39774 ns/op 6625 B/op 7 allocs/op
BenchmarkBytesBufferWithGrow/10-4 1000000 2168 ns/op 160 B/op 2 allocs/op
BenchmarkBytesBufferWithGrow/50-4 200000 5919 ns/op 320 B/op 2 allocs/op
BenchmarkBytesBufferWithGrow/100-4 200000 10095 ns/op 528 B/op 2 allocs/op
BenchmarkBytesBufferWithGrow/200-4 100000 19022 ns/op 1008 B/op 2 allocs/op
BenchmarkBytesBufferWithGrow/500-4 30000 39827 ns/op 2160 B/op 2 allocs/op
PASS
Grow
メソッドであらかじめバッファを確保していた場合は、strings.Builer
の方がアロケーションが少ないことが分かります。また、使用しているメモリも少なく、実行速度もやや早いです。
逆にGrow
メソッドを使わないとパフォーマンスが悪くなっています。
strigns.Builder
はString
メソッドを呼び出した時にアロケーションが発生しません。
内部バッファは可変長の[]byte
型で管理されているため、通常はstring
型に変換した場合に、アロケーションが発生します。
しかし、次のようにポインタを使って型をキャストしているため、アロケーションは発生しません。
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
おわりに
strings.Builder
では、String
メソッドの呼び出し時にアロケーションが発生せず、予めサイズがある程度わかっている場合はbytes.Buffer
に比べてパフォーマンスが良さそうです。
本記事のベンチマークでは、Grow
メソッドを使わない場合は、bytes.Buffer
の方が全体的にパフォーマンスがよくなりましたが、その理由についてはよくわかりません。
実装を比べてみて、理由が分かりましたら、追記したいと思います。