Goでは、string
はプリミティブ型です。つまり読み取り専用であり、それを操作するたびに新しい文字列が作成されます。
結果として得られる文字列の長さを知らずに何度も文字列を連結したいのであれば、それを行うための最良の方法は何ですか?
素朴な方法は次のとおりです。
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
しかし、それはあまり効率的ではないようです。
Go 1.10からはstrings.Builder
型があります、 より詳しくはこの答えを見てください 。
最善の方法は 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)時間で行われます。
文字列を連結する最も効率的な方法は組み込み関数 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)
}
}
Go 1.10以降では、strings.Builder
、 here があります。
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() { ... }
)。
ソースコードを確認してください ここ。
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
私はちょうど私自身のコード(再帰的なツリーウォーク)で上記に投稿された一番上の答えをベンチマークしました、そして、単純な連結演算子は実際には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,"")
}
あなたは大きなバイトのスライスを作成し、文字列スライスを使って短い文字列のバイトをそれにコピーすることができます。 "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 ( )
を使用して、それを再び文字列に変換します。
これは最短の解決策で、最初に全体のバッファサイズを知ったり計算したりする必要はありません。
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%速くなります。
Go 1.10からはstrings.Builder
型があります、 より詳しくはこの答えを見てください 。
@ 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
結論:
CopyPreAllocate
は最速の方法です。 AppendPreAllocate
は1番にかなり近いですが、コードを書く方が簡単です。Concat
はスピードとメモリ使用量の両面で本当に悪いパフォーマンスをしています。使用しないでください。Buffer#Write
とBuffer#WriteString
は、@ Dani-Brがコメントで述べたこととは反対に、基本的にスピードが同じです。 Goではstring
が実際に[]byte
であることを考えると、それは意味があります。Copy
と同じ解決策を使い、追加のブックキーピングやその他のものを使います。Copy
とAppend
は、bytesと同じ64のブートストラップサイズを使用します。Append
はより多くのメモリと割り当てを使用します、それはそれが使う成長アルゴリズムに関連していると思います。バイトほど速くメモリを増やすことはしません。提案:
Append
またはAppendPreAllocate
を使用します。それは十分に速くそして使いやすいです。bytes.Buffer
を使用してください。それがそのために設計されたものです。package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
私の最初の提案は
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
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
これは@iczaと@PickBoyによって言及されたバグの修正と共に@ cd1(Go 1.8
、linux 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
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()
}
私は以下を使用してそれを行います: -
package main
import (
"fmt"
"strings"
)
func main (){
concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.
fmt.Println(concatenation) //abc
}
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))
}
効率的な文字列連結のためにStringBuilder
を持っているJavaの世界から来た人たちにとって、それは最新のgoバージョンがそれと同等であり、それがBuilder
と呼ばれているようです: https://github.com/golang/go/blob/master/ src/strings/builder.go