简单整两句
字符串连接是一个老生常谈的问题了,但因为它在程序中使用的频次实在是太高了,所以这里再次强调一下。
上代码
这段代码使用了几种常见的方式做字符串连接操作,分别是fmt.Sprintf、strings.Builder、bytes.Buffer和+=。
package concat_string
import (
"bytes"
"fmt"
"strconv"
"strings"
"testing"
)
const numbers = 100
func BenchmarkSprintf(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var s string
for i := 0; i < numbers; i++ {
s = fmt.Sprintf("%v%v", s, i)
}
}
b.StopTimer()
}
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var builder strings.Builder
for i := 0; i < numbers; i++ {
builder.WriteString(strconv.Itoa(i))
}
_ = builder.String()
}
b.StopTimer()
}
func BenchmarkBytesBuf(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var buf bytes.Buffer
for i := 0; i < numbers; i++ {
buf.WriteString(strconv.Itoa(i))
}
_ = buf.String()
}
b.StopTimer()
}
func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var s string
for i := 0; i < numbers; i++ {
s += strconv.Itoa(i)
}
}
b.StopTimer()
}
运行这段benchmark测试用例:
$ go test -bench=.
goos: windows
goarch: amd64
pkg: concat_string
BenchmarkSprintf-4 45138 23350 ns/op
BenchmarkStringBuilder-4 1000000 1181 ns/op
BenchmarkBytesBuf-4 751822 1669 ns/op
BenchmarkStringAdd-4 150050 7773 ns/op
PASS
ok concat_string 5.515s
其中我们最常使用的+=和fmt.Sprintf,但这两个最常用的方法性能上却是最差的,一旦我们的程序频繁使用这两种方法,将会造成严重的性能问题。
strings.Builder是Go1.10中引入的,它的性能是几个对比方案中最优的。
使用pprof工具分析一下不同的方案的内存情况:
#生成内存profile文件
$ go test -bench=. -memprofile=mem.prof
goos: windows
goarch: amd64
pkg: concat_string
BenchmarkSprintf-4 49753 23202 ns/op
BenchmarkStringBuilder-4 922437 1180 ns/op
BenchmarkBytesBuf-4 706293 1662 ns/op
BenchmarkStringAdd-4 148202 7870 ns/op
PASS
ok concat_string 6.364s
#分析内存占用情况
$ go tool pprof mem.prof
Type: alloc_space
Time: Feb 27, 2020 at 9:09am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top -cum
Showing nodes accounting for 3090.88MB, 88.21% of 3503.97MB total
Showing top 10 nodes out of 12
flat flat% sum% cum cum%
0 0% 0% 3503.97MB 100% testing.(*B).launch
0 0% 0% 3503.97MB 100% testing.(*B).runN
1465.19MB 41.82% 41.82% 1465.19MB 41.82% concat_string.BenchmarkStringAdd
218MB 6.22% 48.04% 1120.62MB 31.98% concat_string.BenchmarkSprintf
902.62MB 25.76% 73.80% 902.62MB 25.76% fmt.Sprintf
0 0% 73.80% 464.09MB 13.24% concat_string.BenchmarkBytesBuf
0 0% 73.80% 454.07MB 12.96% concat_string.BenchmarkStringBuilder
454.07MB 12.96% 86.76% 454.07MB 12.96% strings.(*Builder).WriteString
0 0% 86.76% 334.07MB 9.53% bytes.(*Buffer).WriteString
51MB 1.46% 88.21% 334.07MB 9.53% bytes.(*Buffer).grow
(pprof)
通过pprof的top命令,我们发现fmt.Sprintf和+=是内存占用最多的,一个合理的解释是,两种方案的内部实现中创建了大量的对象,也就需要大量的垃圾回收(GC),也就更耗性能。
注:
博客内容为极客时间视频课《Go语言从入门到实战》学习笔记。
参考课程链接:
https://time.geekbang.org/course/intro/160?code=NHxez89MnqwIfa%2FvqTiTIaYof1kxYhaEs6o2kf3ZxhU%3D&utm_term=SPoster
博客参考代码:
https://github.com/geektime-geekbang/go_learning/tree/master/code/ch50
若访问github或Google较慢,可使用加速器:
http://91tianlu.date/aff.php?aff=3468