Go中的高效字符串拼接
本文将比较Go语言中各种不同的字符串拼接方法的性能。为了比较,准备了一些基准测试指标,程序如下
func BenchmarkBufferString(b *testing.B, numConcat int) {
var ns string
for i := 0; i < b.N; i++ {
next := nextString()
buffer := bytes.NewBufferString("")
for u := 0; u < numConcat; u++ {
buffer.WriteString(next())
}
ns = buffer.String()
}
global = ns
}
为了测试这些指标,我想需要一个方法返回一些字符串,然后把这些字符串拼接成一个新的字符串。这边写了个简单的方法,每次调用返回当前的索引值:
// nextString is an iterator we use to represent a process
// that returns strings that we want to concatenate in order.
func nextString() func() string {
n := 0
// closure captures variable n
return func() string {
n += 1
return strconv.Itoa(n)
}
}
我们测试的方法都会在循环中调用上面的方法,最后的输入如:0123456789101112131415…。考虑到函数调用的开销,因为都有函数调用开销,所以可以忽略不计。现在已经设置好了测试基准,让我们来看看这些方法的的测试性能
方法1:+=
最直接的拼接字符串的方法是使用+=。由于Go中的字符串是不可变的,所以每次字符串的拼接都会生成新的字符串,分配新的内存来存储,这与C++和Basic是不同的。在Go中,每次拼接2个字符串,都会创建一个新的字符串空间,并拷贝2个字符串。当拼接的字符串长度过长时,拷贝会变的很慢,这种方法的时间复杂度是O(N^2)。
方法2:strings.Join
Go的标准库strings提供了join的方法,该方法可以把一个字符串数组拼接在一起。在本次测试例子中,因为没有字符串数组,所以首先需要构建一个字符串数组,构建字符串数组会消耗一些时间,所以会把这部分时间算在测试基准当中。最终拼接会消耗O(N),但构建字符串消耗了O(N)(平均值),空间消耗O(N)(字符串数组所消耗的空间)。
这边有个使用strings.Join的例子
s := []string{}
for i := 0; i < 1000; i++ {
s = append(s, "a")
}
fmt.Println(strings.Join(s, ""))
方法3:bytes.Buffer
最后一种方式是使用Go中bytes包的Buffer类型,bytes.Buffer实现了io.Writer,可以在O(N)的时间复杂度下完成字符串的拼接,同时无需额外的内存空间来储存字符串数组。
下面是使用bytes.Buffer的例子
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.WriteString("a")
}
fmt.Println(buffer.String())
现在已经了解完了3种字符串拼接的方法,下面开始执行测试,我会先跳过各种测试中的具体过程,直接来看下那个方法最优。
最佳的是 方法3:bytes.Buffer
方法3是最佳的方式,但并不是在所有情况下。下面是比较3种方式,在10000个字符串(小于5个字符),共37KB下时候的测试情况
– 图片 –
上图展示了bytes.Buffer以很大的优势获胜
下面看下每次25个字符,共244KB的情况
– 图片 –
看上去strings.Join和bytes.Buffer差不多,但是还是没bytes.Buffer优秀
我也测试了一些小数量的字符串拼接,这种情况下方法1:+=还是稍微比其他方法快些,所以方法1还是可以用于小量的字符串拼接。
最后,让我们看下方法2中,字符串数组构建的影响有多大。排除字符串数组构建的耗时,同时在遍历时调用nextString,所以测试还是公平的,在来看下测试结果
– 图片 –
当我们忽略方法2的字符串数组构建消耗,那么方法2比方法3略胜一筹。
总结
综上,对于小量的短字符串(少于100个,长度小于10)拼接使用方法1。对于大量字符串,同时需要高效率时,使用方法3,bytes.Buffer是3种方法中最好的。对于需要对已有的字符串数组进行拼接时,使用方法2。
测试过程
下面是对于不同方法,不同拼接数量在我机器(Go 1.2 on Ubuntu 64bit, Intel® Core™ i7-3667U CPU @ 2.00GHz)上执行go test -bench的结果
BenchmarkNaiveConcat10 500000 6403 ns/op
BenchmarkNaiveConcat100 20000 77001 ns/op
BenchmarkNaiveConcat1000 500 3105361 ns/op
BenchmarkNaiveConcat10000 5 236872134 ns/op
BenchmarkJoin10 200000 8129 ns/op
BenchmarkJoin100 50000 51891 ns/op
BenchmarkJoin1000 5000 465179 ns/op
BenchmarkJoin10000 500 5911317 ns/op
BenchmarkBufferString10 200000 7688 ns/op
BenchmarkBufferString100 50000 45027 ns/op
BenchmarkBufferString1000 5000 398311 ns/op
BenchmarkBufferString10000 500 3702906 ns/op
你也可以自己执行下,代码下载https://github.com/hermanschaaf/go-string-concat-benchmarks。 一般的测试基准如下
var global string
// benchmarkNaiveConcat provides a benchmark for basic built-in
// Go string concatenation. Because strings are immutable in Go,
// it performs the worst of the tested methods. The time taken to
// set up the array that is appended is not counted towards the
// time for naive concatenation.
func benchmarkNaiveConcat(b *testing.B, numConcat int) {
var ns string
for i := 0; i < b.N; i++ {
next := nextString()
ns = ""
for u := 0; u < numConcat; u++ {
ns += next()
}
}
// we assign to a global variable to make sure compiler
// or runtime optimizations don't skip over the operations
// we were benchmarking. This might be unnecessary, but it's
// safer.
global = ns
}