strings.Joinの中身を見てみる

strings.Joinの中身を見てみる

// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {
    switch len(elems) {
    case 0:
        return ""
    case 1:
        return elems[0]
    }
    n := len(sep) * (len(elems) - 1)
    for i := 0; i < len(elems); i++ {
        n += len(elems[i])
    }

    var b Builder
    b.Grow(n)
    b.WriteString(elems[0])
    for _, s := range elems[1:] {
        b.WriteString(sep)
        b.WriteString(s)
    }
    return b.String()
}

早期リターン

最初に、Strings.Builferによる結合が必要がないものについてはそのまま要素を返す

   switch len(elems) {
    case 0:
        return ""
    case 1:
        return elems[0]
    }

確保するメモリを前もって計算

Joinの第二引数であるセパレーターの要素数と、結合対象となる文字列スライスの要素数を掛け算し、必要なセパレーターの数を計算 次に文字列スライスの要素数分のだけnに加算していく。

    n := len(sep) * (len(elems) - 1)
    for i := 0; i < len(elems); i++ {
        n += len(elems[i])
    }

メモリの確保

var b Builder
b.Grow(n)

b.Glow(n)で先程計算したメモリ量分だけメモリ領域を拡張する。

// Grow grows b's capacity, if necessary, to guarantee space for
// another n bytes. After Grow(n), at least n bytes can be written to b
// without another allocation. If n is negative, Grow panics.
func (b *Builder) Grow(n int) {
    b.copyCheck()
    if n < 0 {
        panic("strings.Builder.Grow: negative count")
    }
    if cap(b.buf)-len(b.buf) < n {
        b.grow(n)
    }
}

コメント部分和訳

Grow は、必要に応じて b の容量を増やし、さらに n バイト分のスペースを保証します。Grow(n) の後、少なくとも n バイトを b に書き込むことができます。n が負の値の場合、Grow はパニックに陥ります。

メモリ領域が確保されたbuilderに対してappend

結合対象となるスライスとセパレーターの数だけメモリ領域が確保されたbuilderに対し、WriteStringを行って結合処理をする。

    b.WriteString(elems[0])
    for _, s := range elems[1:] {
        b.WriteString(sep)
        b.WriteString(s)
    }
    return b.String()

おまけ

strings.Join内のb.Glow(n)を外したらどうなるのか気になったので実験した。

package main

import (
    "fmt"
    "strconv"
    "strings"
    "testing"
)

var slice []string

func Benchmark(b *testing.B) {
    slice = gen(b, 1000)
    b.ResetTimer()
    BenchmarkJoin(b)
}

func BenchmarkJoin(b *testing.B) {
    fmt.Println(strings.Join(slice, " a "))
}

func gen(b *testing.B, n int) []string {
    var slice []string
    for i := 0; i < n; i++ {
        slice = append(slice, strconv.Itoa(i))
    }
    return slice
}

b.Glow(n)を使わないようにコメントアウト

   var b Builder
    // b.Grow(n)
    b.WriteString(elems[0])

結合処理全体の処理時間におけるベンチマークが以下。 実験時の条件だけ踏まえれば、b.Glow(n)の有無によるベンチマーク差がそのままappend()処理時にスライスの拡張が必要になった場合のメモリアロケーションによるベンチマーク差と一致する。

b.Glow(n)あり b.Glow(n)なし
n=100 0.000017 ns/op 0.000024 ns/op 0.000007 ns/op
n=1000 0.000044 ns/op 0.000064 ns/op 0.000020 ns/op
n=10000 0.000221 ns/op 0.000586 ns/op 0.000365 ns/op
n=100000 0.008440 ns/op 0.010600 ns/op 0.002160 ns/op
n=1000000 0.129000 ns/op 0.135000 ns/op 0.006000 ns/op