標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

1byte ではなく、1 runeずつ文字列を伸ばしてゆく一番速い方法は?

(追記)最終的には 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 は本当にコストがかかるんだなぁ