web-dev-qa-db-ja.com

Goで文字列を効率的に連結する方法

Goでは、stringはプリミティブ型です。つまり読み取り専用であり、それを操作するたびに新しい文字列が作成されます。

結果として得られる文字列の長さを知らずに何度も文字列を連結したいのであれば、それを行うための最良の方法は何ですか?

素朴な方法は次のとおりです。

s := ""
for i := 0; i < 1000; i++ {
    s += getShortStringFromSomewhere()
}
return s

しかし、それはあまり効率的ではないようです。

620

2018年に追加されたメモ

Go 1.10からはstrings.Builder型があります、 より詳しくはこの答えを見てください

201x以前の答え

最善の方法は bytes パッケージを使うことです。 io.Writer を実装する Buffer 型があります。

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString("a")
    }

    fmt.Println(buffer.String())
}

これはO(n)時間で行われます。

773
marketer

文字列を連結する最も効率的な方法は組み込み関数 copy を使うことです。私のテストでは、このアプローチは bytes.Buffer を使用するよりも約3倍速く、演算子+を使用するよりもはるかに高速です(約12,000倍)。また、それはより少ないメモリを使用します。

これを証明するために テストケース を作成しました。結果は次のとおりです。

BenchmarkConcat  1000000    64497 ns/op   502018 B/op   0 allocs/op
BenchmarkBuffer  100000000  15.5  ns/op   2 B/op        0 allocs/op
BenchmarkCopy    500000000  5.39  ns/op   0 B/op        0 allocs/op

以下はテスト用のコードです。

package main

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkConcat(b *testing.B) {
    var str string
    for n := 0; n < b.N; n++ {
        str += "x"
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); str != s {
        b.Errorf("unexpected result; got=%s, want=%s", str, s)
    }
}

func BenchmarkBuffer(b *testing.B) {
    var buffer bytes.Buffer
    for n := 0; n < b.N; n++ {
        buffer.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); buffer.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s)
    }
}

func BenchmarkCopy(b *testing.B) {
    bs := make([]byte, b.N)
    bl := 0

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        bl += copy(bs[bl:], "x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); string(bs) != s {
        b.Errorf("unexpected result; got=%s, want=%s", string(bs), s)
    }
}

// Go 1.10
func BenchmarkStringBuilder(b *testing.B) {
    var strBuilder strings.Builder

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        strBuilder.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); strBuilder.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s)
    }
}
258
cd1

Go 1.10以降では、strings.Builderhere があります。

Builderは、Writeメソッドを使用して文字列を効率的に構築するために使用されます。メモリのコピーを最小限に抑えます。ゼロ値はすぐに使用できます。


使用法:

bytes.Bufferでもほぼ同じです。

package main

import (
    "strings"
    "fmt"
)

func main() {
    var str strings.Builder

    for i := 0; i < 1000; i++ {
        str.WriteString("a")
    }

    fmt.Println(str.String())
}

注:StringBuilder値は、基になるデータをキャッシュするため、コピーしないでください。 StringBuilder値を共有する場合は、ポインターを使用します。


サポートするStringBuilderメソッドとインターフェイス:

そのメソッドは既存のインターフェイスを念頭に置いて実装されているため、コードで新しいBuilderに簡単に切り替えることができます。


ゼロ値の使用:

var buf strings.Builder

バイトとの違いBuffer:

  • 成長またはリセットのみ可能です。

  • bytes.Bufferでは、次のように基礎となるバイトにアクセスできます:(*Buffer).Bytes(); strings.Builderはこの問題を防ぎます。ただし、これは問題ではなく、代わりに望ましい場合もあります(たとえば、バイトがio.Readerに渡されるときのピーク動作のため)。

  • また、誤ってコピーすることを防ぐcopyCheckメカニズムが組み込まれています(func (b *Builder) copyCheck() { ... })。


ソースコードを確認してください ここ

158
Inanc Gumus

StringsパッケージにはJoinというライブラリ関数があります。 http://golang.org/pkg/strings/#Join

Joinのコードを見ると、Kinopikoが追加したAppend関数に対する同様のアプローチがわかります: https://golang.org/src/strings/strings.go#L420

使用法:

import (
    "fmt";
    "strings";
)

func main() {
    s := []string{"this", "is", "a", "joined", "string\n"};
    fmt.Printf(strings.Join(s, " "));
}

$ ./test.bin
this is a joined string
124
mbarkhau

私はちょうど私自身のコード(再帰的なツリーウォーク)で上記に投稿された一番上の答えをベンチマークしました、そして、単純な連結演算子は実際にはBufferStringより速いです。

func (r *record) String() string {
    buffer := bytes.NewBufferString("");
    fmt.Fprint(buffer,"(",r.name,"[")
    for i := 0; i < len(r.subs); i++ {
        fmt.Fprint(buffer,"\t",r.subs[i])
    }
    fmt.Fprint(buffer,"]",r.size,")\n")
    return buffer.String()
}

これには0.81秒かかりましたが、次のコードでは

func (r *record) String() string {
    s := "(\"" + r.name + "\" ["
    for i := 0; i < len(r.subs); i++ {
        s += r.subs[i].String()
    }
    s += "] " + strconv.FormatInt(r.size,10) + ")\n"
    return s
} 

0.61秒しかかかりませんでした。これはおそらく、新しいBufferStringを作成することによるオーバーヘッドが原因です。

更新: /私はjoin関数もベンチマークしました、そしてそれは0.54秒で走りました。

func (r *record) String() string {
    var parts []string
    parts = append(parts, "(\"", r.name, "\" [" )
    for i := 0; i < len(r.subs); i++ {
        parts = append(parts, r.subs[i].String())
    }
    parts = append(parts, strconv.FormatInt(r.size,10), ")\n")
    return strings.Join(parts,"")
}
39
JasonMc

あなたは大きなバイトのスライスを作成し、文字列スライスを使って短い文字列のバイトをそれにコピーすることができます。 "Effective Go"で与えられた関数があります:

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2);
        // Copy data (could use bytes.Copy()).
        for i, c := range slice {
            newSlice[i] = c
        }
        slice = newSlice;
    }
    slice = slice[0:l+len(data)];
    for i, c := range data {
        slice[l+i] = c
    }
    return slice;
}

その後、操作が終了したら、大きなバイトスライスにstring ( )を使用して、それを再び文字列に変換します。

21
user181548

これは最短の解決策で、最初に全体のバッファサイズを知ったり計算したりする必要はありません。

var data []byte
for i := 0; i < 1000; i++ {
    data = append(data, getShortStringFromSomewhere()...)
}
return string(data)

私の ベンチマーク によると、コピーソリューションよりも20%遅くなります(6.72nsではなく、8.1nsあたり8.1ns)が、bytes.Bufferを使うよりも55%速くなります。

20
rog

2018年に追加されたメモ

Go 1.10からはstrings.Builder型があります、 より詳しくはこの答えを見てください

201x以前の答え

@ cd1のベンチマークコードと他の答えは間違っています。 b.Nはベンチマーク関数で設定されるべきではありません。テストの実行時間が安定しているかどうかを判断するためにgoテストツールによって動的に設定されます。

ベンチマーク関数は同じテストをb.N回実行し、ループ内のテストは各反復で同じでなければなりません。だから私は内側のループを追加することでそれを修正します。私はまた他の解決策のベンチマークを追加します。

package main

import (
    "bytes"
    "strings"
    "testing"
)

const (
    sss = "xfoasneobfasieongasbg"
    cnt = 10000
)

var (
    bbb      = []byte(sss)
    expected = strings.Repeat(sss, cnt)
)

func BenchmarkCopyPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        bs := make([]byte, cnt*len(sss))
        bl := 0
        for i := 0; i < cnt; i++ {
            bl += copy(bs[bl:], sss)
        }
        result = string(bs)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppendPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, cnt*len(sss))
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        buf := bytes.NewBuffer(make([]byte, 0, cnt*len(sss)))
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkCopy(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Buffer
        for i := 0; i < cnt; i++ {
            off := len(data)
            if off+len(sss) > cap(data) {
                temp := make([]byte, 2*cap(data)+len(sss))
                copy(temp, data)
                data = temp
            }
            data = data[0 : off+len(sss)]
            copy(data[off:], sss)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppend(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64)
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWrite(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.Write(bbb)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWriteString(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkConcat(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < cnt; i++ {
            str += sss
        }
        result = str
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

環境はOS X 10.11.6、2.2 GHz Intel Core i7です。

試験結果:

BenchmarkCopyPreAllocate-8         20000             84208 ns/op          425984 B/op          2 allocs/op
BenchmarkAppendPreAllocate-8       10000            102859 ns/op          425984 B/op          2 allocs/op
BenchmarkBufferPreAllocate-8       10000            166407 ns/op          426096 B/op          3 allocs/op
BenchmarkCopy-8                    10000            160923 ns/op          933152 B/op         13 allocs/op
BenchmarkAppend-8                  10000            175508 ns/op         1332096 B/op         24 allocs/op
BenchmarkBufferWrite-8             10000            239886 ns/op          933266 B/op         14 allocs/op
BenchmarkBufferWriteString-8       10000            236432 ns/op          933266 B/op         14 allocs/op
BenchmarkConcat-8                     10         105603419 ns/op        1086685168 B/op    10000 allocs/op

結論:

  1. CopyPreAllocateは最速の方法です。 AppendPreAllocateは1番にかなり近いですが、コードを書く方が簡単です。
  2. Concatはスピードとメモリ使用量の両面で本当に悪いパフォーマンスをしています。使用しないでください。
  3. Buffer#WriteBuffer#WriteStringは、@ Dani-Brがコメントで述べたこととは反対に、基本的にスピードが同じです。 Goではstringが実際に[]byteであることを考えると、それは意味があります。
  4. bytes.Bufferは基本的にCopyと同じ解決策を使い、追加のブックキーピングやその他のものを使います。
  5. CopyAppendは、bytesと同じ64のブートストラップサイズを使用します。
  6. Appendはより多くのメモリと割り当てを使用します、それはそれが使う成長アルゴリズムに関連していると思います。バイトほど速くメモリを増やすことはしません。

提案:

  1. OPが望んでいるような単純な作業には、AppendまたはAppendPreAllocateを使用します。それは十分に速くそして使いやすいです。
  2. バッファを同時に読み書きする必要がある場合は、もちろんbytes.Bufferを使用してください。それがそのために設計されたものです。
19
PickBoy
package main

import (
  "fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}
16
harold ramos

私の最初の提案は

s12 := fmt.Sprint(s1,s2)

しかし、上記の bytes.Buffer - WriteString() を使った答えが最も効率的な方法です。

私の最初の提案はリフレクションとタイプスイッチを使います。 (p *pp) doPrintおよび(p *pp) printArgを参照
私が素朴に考えたように、基本的な型のための普遍的なStringer()インターフェースはありません。

少なくとも、Sprint() internal はbytes.Bufferを使います。このように

`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`

メモリ割り当てに関しては許容範囲です。

=> Sprint()連結は迅速なデバッグ出力に使用できます。
=>それ以外の場合はbytesを使用します。Buffer... WriteString

12
Peter Buchmann

Cd1の答えを拡張する:あなたはcopy()の代わりにappend()を使うかもしれません。 append()を使用すると、メモリが少し増えますが、時間が節約されます。 さらに2つのベンチマーク をあなたの一番上に追加しました。でローカルに実行

go test -bench=. -benchtime=100ms

私のThinkPad T400では、次のようになります。

BenchmarkAppendEmpty    50000000         5.0 ns/op
BenchmarkAppendPrealloc 50000000         3.5 ns/op
BenchmarkCopy           20000000        10.2 ns/op
10
Peter Buchmann

これは@iczaと@PickBoyによって言及されたバグの修正と共に@ cd1(Go 1.8linux x86_64)によって提供されるベンチマークの実際のバージョンです。

Bytes.Bufferは、7演算子による直接文字列連結よりも+倍高速です。

package performance_test

import (
    "bytes"
    "fmt"
    "testing"
)

const (
    concatSteps = 100
)

func BenchmarkConcat(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < concatSteps; i++ {
            str += "x"
        }
    }
}

func BenchmarkBuffer(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var buffer bytes.Buffer
        for i := 0; i < concatSteps; i++ {
            buffer.WriteString("x")
        }
    }
}

タイミング:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op
2
Vitaly Isaev

goutils.JoinBetween

 func JoinBetween(in []string, separator string, startIndex, endIndex int) string {
    if in == nil {
        return ""
    }

    noOfItems := endIndex - startIndex

    if noOfItems <= 0 {
        return EMPTY
    }

    var builder strings.Builder

    for i := startIndex; i < endIndex; i++ {
        if i > startIndex {
            builder.WriteString(separator)
        }
        builder.WriteString(in[i])
    }
    return builder.String()
}
1
Xian Shu

私は以下を使用してそれを行います: -

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}
1
package main

import (
"fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    result := make([]byte, 0)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)

    fmt.Println(string(result))
}
0
rajni kant

効率的な文字列連結のためにStringBuilderを持っているJavaの世界から来た人たちにとって、それは最新のgoバージョンがそれと同等であり、それがBuilderと呼ばれているようです: https://github.com/golang/go/blob/master/ src/strings/builder.go

0
Joel