如何在Go中高效地连接string?
在Go中,string是一个原始types,它是只读的,对它的每一个操作都会创build一个新的string。
所以,如果我想多次连接string而不知道结果string的长度,那么最好的方法是什么?
天真的方式是:
s := "" for i := 0; i < 1000; i++ { s += getShortStringFromSomewhere() } return s
但这似乎不是很有效。
最好的方法是使用bytes
包。 它有一个实现io.Writer
的Buffer
types。
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)时间。
连接string的最有效方法是使用内置函数copy
。 在我的testing中,这个方法比使用bytes.Buffer
快了3倍(〜12,000x)比使用operator +
更快。 而且,它使用更less的内存。
我创build了一个testing用例来certificate这一点,下面是结果:
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
以下是testing代码:
package main import ( "bytes" "strings" "testing" ) func BenchmarkConcat(b *testing.B) { var str string for n := 0; n < bN; n++ { str += "x" } b.StopTimer() if s := strings.Repeat("x", bN); 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 < bN; n++ { buffer.WriteString("x") } b.StopTimer() if s := strings.Repeat("x", bN); buffer.String() != s { b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s) } } func BenchmarkCopy(b *testing.B) { bs := make([]byte, bN) bl := 0 b.ResetTimer() for n := 0; n < bN; n++ { bl += copy(bs[bl:], "x") } b.StopTimer() if s := strings.Repeat("x", bN); string(bs) != s { b.Errorf("unexpected result; got=%s, want=%s", string(bs), s) } }
string包中有一个名为Join
的库函数: http : //golang.org/pkg/strings/#Join
看看Join
的代码显示类似的方法来追加函数Kinopiko写道: https ://golang.org/src/strings/strings.go#L462
用法:
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
我只是在我自己的代码(recursion树行)中testing了上面发布的顶级答案,而简单的concat操作符实际上比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.81s,而下面的代码:
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.61s。 这可能是由于创build新的BufferStrings的开销。
更新:我也基准连接function,它运行在0.54s
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,"") }
这是最快的解决scheme,不需要您首先知道或计算整个缓冲区大小:
var data []byte for i := 0; i < 1000; i++ { data = append(data, getShortStringFromSomewhere()...) } return string(data)
按照我的基准testing ,它比复制解决scheme慢了20%(每附加8.1ns而不是6.72ns),但仍比使用字节快55%。缓冲。
您可以创build一个大的字节片段,并使用string切片将短string的字节复制到其中。 “Effective Go”中有一个function:
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 ( )
将其再次转换为string。
package main import ( "fmt" ) func main() { var str1 = "string1" var str2 = "string2" out := fmt.Sprintf("%s %s ",str1, str2) fmt.Println(out) }
扩展cd1的答案:你可以使用append()而不是copy()。 append()使得提前规定更大,花费更多的内存,但节省了时间。 我在你的顶部添加了两个基准 。 用本地运行
go test -bench=. -benchtime=100ms
在我的thinkpad T400s上,它产生了:
BenchmarkAppendEmpty 50000000 5.0 ns/op BenchmarkAppendPrealloc 50000000 3.5 ns/op BenchmarkCopy 20000000 10.2 ns/op
我原来的build议是
s12 := fmt.Sprint(s1,s2)
但上面的答案使用bytes.Buffer – WriteString()是最有效的方法。
我最初的build议使用reflection和types开关。 请参阅(p *pp) doPrint
和(p *pp) printArg
基本types没有通用的Stringer()接口,就像我天真地想的那样。
至less,Sprint()在内部使用了一个bytes.Buffer。 从而
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
在内存分配方面是可以接受的。
=> Sprint()串联可用于快速debugging输出。
=>否则使用bytes.Buffer … WriteString
@ cd1和其他答案的基准代码是错误的。 bN
不应该设置在基准函数中。 它由dynamictesting工具dynamic设置,以确定testing的执行时间是否稳定。
基准函数应该运行相同的testing次数,并且循环内部的testing对于每次迭代应该是相同的。 所以我通过添加一个内部循环来修复它。 我还为其他一些解决scheme添加了基准:
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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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
非常接近No.1,但编写代码更容易。 - 对于速度和内存使用情况来说,
Concat
性能非常差。 不要使用它。 -
Buffer#Write
和Buffer#WriteString
速度基本相同,与Dani-Br在评论中所说的相反。 考虑到string
确实是Go中的[]byte
,这是有道理的。 - bytes.Buffer基本上使用与
Copy
附加书籍和其他东西相同的解决scheme。 -
Copy
并Append
使用64的引导程序大小,与bytes.Buffer相同 -
Append
使用更多的内存和分配,我认为这与它使用的增长algorithm有关。 它不像字节那样快地增长内存。缓冲区
build议:
- 对于简单的任务,比如OP想要的,我会使用
Append
或者AppendPreAllocate
。 它足够快,易于使用。 - 如果需要同时读写缓冲区,当然使用
bytes.Buffer
。 这就是它的目的。
这是@ cd1( Go 1.8
, linux x86_64
)提供的基准testing的实际版本,修复了@icza和@PickBoy提到的错误。
Bytes.Buffer
只比直接string连接通过+
运算符快7
倍。
package performance_test import ( "bytes" "fmt" "testing" ) const ( concatSteps = 100 ) func BenchmarkConcat(b *testing.B) { for n := 0; n < bN; n++ { var str string for i := 0; i < concatSteps; i++ { str += "x" } } } func BenchmarkBuffer(b *testing.B) { for n := 0; n < bN; 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
看一看golang的strconv库,可以访问几个AppendXX函数,使我们能够将string与string和其他数据types连接起来。
[Off Topic]查看这个博客,了解一些golang的function
从“strings”包中joinstrings.Join()
如果你有一个types不匹配(就像你想join一个int和一个string一样),你可以使用RANDOMTYPE(你想改变的东西)
EX:
package main import "strings" var intEX = 0 var stringEX = "hello all you " var stringEX2 = " people in here" func main() { strings.Join(stringEX, string(intEX), stringEX2) }
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))