(追記)最終的には strings.Builder の優勝です。最後まで読みましょう
「1 byte ずつ」というのは今まで何回も計測されてきて、
- append > strings.Builder > 文字列の加算
というのが定説になっている。が、「1 rune ずつ」の場合はどうだろうか?
というわけで計測してみた。
// main_test.go package runebuffer import ( "strings" "testing" ) // 定番の strings.Builder func join1(L []rune) string { var buffer strings.Builder for _, r := range L { buffer.WriteRune(r) } return buffer.String() } // append で rune のスライスを伸ばして、最後に string 化 func join2(L []rune) string { buffer := make([]rune, 0, 10) for _, r := range L { buffer = append(buffer, r) } return string(buffer) } // rune を string 化してから、[]byte に append して、最後に string 化 func join3(L []rune) string { buffer := make([]byte, 0, 30) for _, r := range L { buffer = append(buffer, string(r)...) } return string(buffer) } func test(b *testing.B, f func([]rune) string) { for i := 0; i < b.N; i++ { f([]rune{'あ', 'い', 'う', 'え', 'お'}) } } func Benchmark1(b *testing.B) { test(b, join1) } func Benchmark2(b *testing.B) { test(b, join2) } func Benchmark3(b *testing.B) { test(b, join3) }
$ go test -bench . goos: windows goarch: amd64 Benchmark1-4 6095110 175 ns/op Benchmark2-4 8638275 137 ns/op Benchmark3-4 9762049 121 ns/op PASS ok _/C_/Users/hymko/runebuild 4.186s
rune を string 化してから、[]byte に append して、最後に string 化するのが一番速いか。ただし、事前にちゃんとバッファのキャパシティーを広めに確保(make([]byte,0,30)
) しておくのは必須。
追記
allocation 回数的にも有利だった。
$ go test -bench . -benchmem goos: windows goarch: amd64 Benchmark1-4 6822398 165 ns/op 80 B/op 4 allocs/op Benchmark2-4 8828917 139 ns/op 64 B/op 2 allocs/op Benchmark3-4 10006144 120 ns/op 48 B/op 2 allocs/op PASS ok _/C_/Users/hymko/runebuild 4.273s
さらに追記
strings.Builder版だけ、事前の領域確保が少ないせいで遅いのでは疑惑があったので、
var buffer strings.Builder buffer.Grow(30)
を入れてリトライしてみた。
goos: windows goarch: amd64 Benchmark1-4 9995352 111 ns/op 64 B/op 2 allocs/op Benchmark2-4 8700891 138 ns/op 64 B/op 2 allocs/op Benchmark3-4 9842050 123 ns/op 48 B/op 2 allocs/op PASS ok _/C_/Users/hymko/runebuild 5.074s
Oh...逆転してしまった。allocation は本当にコストがかかるんだなぁ