内容简介:在上一篇关于字符串拼接的文章在开始前给大家送个福利。在上一篇的文章的末尾,我已经提出了2个可能性:拼接字符串的数量和拼接字符串的大小,现在我们就开始证明这两种情况,为了演示方便,我们把原来的拼接函数修改一下,可以接受一个
在上一篇关于字符串拼接的文章 Go语言字符串高效拼接(一) 中,我们演示的多种字符串拼接的方式,并且使用一个例子来测试了他们的性能,通过对比发现,我们觉得性能高的 Builder
并未发挥出其应该的性能,反而 +
号拼接,甚至 strings.Join
方法的性能更优越,那么这到底是什么原因呢?今天我们开始解开他们神秘的面纱,解开谜底。
在开始前给大家送个福利。 阿里云双11拼团活动,战队已达数百人,前三十名,已开始瓜分百万奖金,赶紧加入 。 现在加入即可享受最低1折,1年99元的云主机,已经可以参与瓜分百万奖金,阿里云双11战队排30名,先邀请再购买,赶紧上车,老司机开车。
拼接函数改造
在上一篇的文章的末尾,我已经提出了2个可能性:拼接字符串的数量和拼接字符串的大小,现在我们就开始证明这两种情况,为了演示方便,我们把原来的拼接函数修改一下,可以接受一个 []string
类型的参数,这样我们就可以对切片数组进行字符串拼接,这里直接给出所有的拼接方法的改造后实现。
func StringPlus(p []string) string{ var s string l:=len(p) for i:=0;i<l;i++{ s+=p[i] } return s } func StringFmt(p []interface{}) string{ return fmt.Sprint(p...) } func StringJoin(p []string) string{ return strings.Join(p,"") } func StringBuffer(p []string) string { var b bytes.Buffer l:=len(p) for i:=0;i<l;i++{ b.WriteString(p[i]) } return b.String() } func StringBuilder(p []string) string { var b strings.Builder l:=len(p) for i:=0;i<l;i++{ b.WriteString(p[i]) } return b.String() } 复制代码
以上实现中的 for
循环我并没有使用 for range
,为了提高性能,具体原因请参考我的 Go语言性能优化- For Range 性能研究 。
测试用例
以上的字符串拼接函数修改后,我们就可以构造不同大小的切片进行字符串拼接测试了。为了模拟上次的效果,我们先用10个切片大小的字符串进行拼接测试,和上一篇的测试情形差不多(也是大概10个字符串拼接)。
const BLOG = "http://www.flysnow.org/" func initStrings(N int) []string{ s:=make([]string,N) for i:=0;i<N;i++{ s[i]=BLOG } return s; } func initStringi(N int) []interface{}{ s:=make([]interface{},N) for i:=0;i<N;i++{ s[i]=BLOG } return s; } 复制代码
这是两个构建测试用力切片数组的函数,可以生成N个大小的切片。第二个 initStringi
函数返回的是 []interface{}
,这是专门为 StringFmt(p []interface{})
拼接函数准备的,减少类型之间的转换。
有了这两个生成测试用例的函数,我们就可以构建我们的 Go 语言性能测试了,我们先测试10个大小的切片。
func BenchmarkStringPlus10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringPlus(p) } } func BenchmarkStringFmt10(b *testing.B) { p:= initStringi(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringFmt(p) } } func BenchmarkStringJoin10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringJoin(p) } } func BenchmarkStringBuffer10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuffer(p) } } func BenchmarkStringBuilder10(b *testing.B) { p:= initStrings(10) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuilder(p) } } 复制代码
在每个性能测试函数中,我们都会调用 b.ResetTimer()
,这是为了避免测试用例准备时间不同,带来的性能测试效果偏差问题,具体可以参考我的一篇文章 Go语言实战笔记(二十二)| Go 基准测试 。
我们运行 go test -bench=. -run=NONE -benchmem
查看结果。
BenchmarkStringPlus10-8 3000000 593 ns/op 1312 B/op 9 allocs/op BenchmarkStringFmt10-8 5000000 335 ns/op 240 B/op 1 allocs/op BenchmarkStringJoin10-8 10000000 200 ns/op 480 B/op 2 allocs/op BenchmarkStringBuffer10-8 3000000 452 ns/op 864 B/op 4 allocs/op BenchmarkStringBuilder10-8 10000000 231 ns/op 480 B/op 4 allocs/op 复制代码
通过这次我们可以看到, +
号拼接不再具有优势,因为 string
是不可变的,每次拼接都会生成一个新的 string
,也就是会进行一次内存分配,我们现在是10个大小的切片,每次操作要进行9次进行分配,占用内存,所以每次操作时间都比较长,自然性能就低下。
可能有读者记得,我们上一篇文章 Go语言字符串高效拼接(一) 中, +
加号拼接的性能测试中显示的只有2次内存分配,但是我们用了好多个 +
的。
func StringPlus() string{ var s string s+="昵称"+":"+"飞雪无情"+"\n" s+="博客"+":"+"http://www.flysnow.org/"+"\n" s+="微信公众号"+":"+"flysnow_org" return s } 复制代码
再来回顾下这段代码,的确是有很多 +
的,但是只有2次内存分配,我们可以大胆猜测,是3次 s+=
导致的,正常和我们今天测试的10个长度的切片,只有9次内存分配一样。下面我们通过运行如下命令看下Go编译器对这段代码的优化: go build -gcflags="-m -m" main.go
,输出中有如下内容:
can inline StringPlus as: func() string { var s string; s = <N>; s += "昵称:飞雪无情\n"; s += "博客:http://www.flysnow.org/\n"; s += "微信公众号:flysnow_org"; return s } 复制代码
现在一目了然了,其实是编译器帮我们把字符串做了优化,只剩下3个 s+=
这次,采用长度为10个切片进行测试,也很明显测试出了 Builder
要比 Buffer
性能好很多,这个问题原因主要还是 []byte
和 string
之间的转换, Builder
恰恰解决了这个问题。
func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) } 复制代码
很高效的解决方案。
100个字符串
现在我们测试下100个字符串拼接的情况,对于我们上面的代码,要改造非常容易,这里直接给出测试代码。
func BenchmarkStringPlus100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringPlus(p) } } func BenchmarkStringFmt100(b *testing.B) { p:= initStringi(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringFmt(p) } } func BenchmarkStringJoin100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringJoin(p) } } func BenchmarkStringBuffer100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuffer(p) } } func BenchmarkStringBuilder100(b *testing.B) { p:= initStrings(100) b.ResetTimer() for i:=0;i<b.N;i++{ StringBuilder(p) } } 复制代码
现在运行性能测试,看看100个字符串连接的性能怎么样,哪个函数最高效。
BenchmarkStringPlus100-8 100000 19711 ns/op 123168 B/op 99 allocs/op BenchmarkStringFmt100-8 500000 2615 ns/op 2304 B/op 1 allocs/op BenchmarkStringJoin100-8 1000000 1516 ns/op 4608 B/op 2 allocs/op BenchmarkStringBuffer100-8 500000 2333 ns/op 8112 B/op 7 allocs/op BenchmarkStringBuilder100-8 1000000 1714 ns/op 6752 B/op 8 allocs/op 复制代码
+
号和我们上面分析得一样,这次是99次内存分配,性能体验越来越差,在后面的测试中,会排除掉。
fmt
和 bufrer
已经的性能也没有提升,继续走低。剩下比较坚挺的是 Join
和 Builder
。
1000 个字符串。
测试用力和上面章节的大同小异,所以我们直接看测试结果。
BenchmarkStringPlus1000-8 1000 1611985 ns/op 12136228 B/op 999 allocs/op BenchmarkStringFmt1000-8 50000 28510 ns/op 24590 B/op 1 allocs/op BenchmarkStringJoin1000-8 100000 15050 ns/op 49152 B/op 2 allocs/op BenchmarkStringBuffer1000-8 100000 23534 ns/op 122544 B/op 11 allocs/op BenchmarkStringBuilder1000-8 100000 17996 ns/op 96224 B/op 16 allocs/op 复制代码
整体和100个字符串的时候差不多,表现好的还是 Join
和 Builder
。这两个方法的使用侧重点有些不一样, 如果有现成的数组、切片那么可以直接使用 Join
,但是如果没有,并且追求灵活性拼接,还是选择 Builder
。 Join
还是定位于有现成切片、数组的(毕竟拼接成数组也要时间),并且使用固定方式进行分解的,比如逗号、空格等,局限比较大。
小结
至于10000个字符串拼接我这里就不做测试了,大家可以自己试试,看看是不是大同小异的。
从最近的这两篇文章的分析来看,我们大概可以总结出。
-
+
连接适用于短小的、常量字符串(明确的,非变量),因为编译器会给我们优化。 -
Join
是比较统一的拼接,不太灵活 -
fmt
和buffer
基本上不推荐 -
builder
从性能和灵活性上,都是上佳的选择。
到这里就完了吗?这篇文章是完了,我也该睡觉了。但是字符串高效拼接还没完,以上并不是终极性能,还可以优化,敬请期待第三篇。
本文为原创文章,转载注明出处,「总有烂人抓取文章的时候还去掉我的原创说明」欢迎扫码关注公众号 flysnow_org
或者网站www.flysnow.org/,第一时间看后续精彩文章。「防烂人备注**……&*¥」觉得好的话,顺手分享到朋友圈吧,感谢支持。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。