假设我们现在要实现这样一个拼接函数: 将字符串重复n次拼接起来,返回一个新字符串。
+
运算符func simpleSplice(s string, n int) string {
newStr := ""
for i := 0; i < n; i++ {
newStr += s
}
return newStr
}
Sprintf
func sprintfSplice(s string, n int) string {
newStr := ""
for i := 0; i < n; i++ {
newStr = fmt.Sprintf("%s%s", newStr, s)
}
return newStr
}
[]byte
func bytesSplice(s string, n int) string {
newStr := []byte{}
for i := 0; i < n; i++ {
newStr = append(newStr, []byte(s)...)
}
return string(newStr)
}
bytes.Buffer
func bufferSplice(s string, n int) string {
buffer := bytes.Buffer{}
for i := 0; i < n; i++ {
buffer.WriteString(s)
}
return buffer.String()
}
strings.Builder
func builderSplice(s string, n int) string {
builder := strings.Builder{}
for i := 0; i < n; i++ {
builder.WriteString(s)
}
return builder.String()
}
我们对上面五种方法进行benchmark测试。
测试函数类似这样,生成100长度的随机字符串,然后拼接100次。
func BenchmarkBytesSplice3(b *testing.B) {
for i := 0; i < b.N; i++ {
bytesSplice3(genStr(100), 100)
}
}
结果如下:
BenchmarkSimpleSplice
BenchmarkSimpleSplice-12 9901 123436 ns/op
BenchmarkSprintfSplice
BenchmarkSprintfSplice-12 8151 144824 ns/op
BenchmarkBytesSplice
BenchmarkBytesSplice-12 62271 19435 ns/op
BenchmarkBuilderSplice
BenchmarkBuilderSplice-12 93918 11890 ns/op
BenchmarkBufferSplice
BenchmarkBufferSplice-12 97413 11816 ns/op
以上仅测试了一次, 不要求严谨, 只是为了说明问题
可以看见使用+
运算符拼接和Sprintf
的性能是最差的。
+
运算符/Sprintf+
运算符和Sprintf
性能差的原因其实差不多: 每次都会创建一个新的字符串,然后将原来的字符串和新的字符串拼接起来,这样就会产生很多的临时字符串,这些临时字符串会占用很多的内存,而且还会增加GC的负担。
这三者都是利用[]byte
实现的功能,所以从当前这个测试中来看差别不大(对比上面那两个来说~)。
我们查看buffer
的源码发现, 他每次写入之前会计算好所需的空间, 然后将其copy
到[]byte
中。
而Builder
中虽然使用append
追加的数据, 但是使用了unsafe
方法直接操作内存指针。
所以操作[]byte
来实现拼接是最快, 而buffer
和Builder
带来的内存优化有多少呢?
我们调大需要测试的字符串长度, 这样会更明显: 字符串长度1000, 拼接1000次。
func BenchmarkBytesSplice3(b *testing.B) {
for i := 0; i < b.N; i++ {
bytesSplice3(genStr(1000), 1000)
}
}
运行结果
BenchmarkBytesSplice3-12 2421 521164 ns/op 2033674 B/op 1003 allocs/op
BenchmarkBuilderSplice-12 1135 986065 ns/op 5238292 B/op 26 allocs/op
BenchmarkBufferSplice-12 2733 451784 ns/op 3105806 B/op 14 allocs/op
BytesSplice3
为什么会有1003 allocs ? 明明已经预分配内存了
func bytesSplice3(s string, n int) string {
// 预分配内存
newStr := make([]byte, 0, len(s)*n)
for i := 0; i < n; i++ {
// 问题在这里, 每次都会创建一个新的[]byte
// 其实这个动作可以省略掉
newStr = append(newStr, []byte(s)...)
// 更换成这种写法试试
// newStr = append(newStr, s...)
}
return *(*string)(unsafe.Pointer(&newStr))
}
更换后:
BenchmarkBytesSplice3-12 8913 135231 ns/op 1009667 B/op 3 allocs/op
BenchmarkBuilderSplice-12 1530 756342 ns/op 5238292 B/op 26 allocs/op
BenchmarkBufferSplice-12 3198 381564 ns/op 3105808 B/op 14 allocs/op
在可预知拼接结果长度的情况下, 使用make([]byte, 0, len(s)*n)
这样的方式来预分配内存是最合适的。 其他情况下, 使用strings.Builder
, buffer
都是可以的。