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

Go1.10で入るstrings.Builderを検証した #golang

More than 1 year has passed since last update.

はじめに

2018年2月にリリース予定のGo1.10では、strings.Builderという型が(CL74931)が入ります。
このstrings.Builderを用いると、効率良く文字列を生成することができます。
この記事では、まずstrings.Builderの使い方を説明し、次に同様の使い方ができるbytes.Bufferとベンチマークを取ってパフォーマンスを比較してみます。
そしてさらに、strings.Builderの実装をソースコードから読み解き、そこで使われているテクニックを解説します。

使い方

まずは使い方をみていきましょう。
strings.Builderはゼロ値で扱える型です。そのため、使用するのに初期化は特にいらず、次のように変数を定義するだけで使用することができます。

var b strings.Builder

strings.Builderio.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.Builderbytes.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.BuilderStringメソッドを呼び出した時にアロケーションが発生しません。
内部バッファは可変長の[]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の方が全体的にパフォーマンスがよくなりましたが、その理由についてはよくわかりません。
実装を比べてみて、理由が分かりましたら、追記したいと思います。

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
ユーザーは見つかりませんでした