内容简介:在上一篇关于字符串拼接的文章在开始前给大家送个福利。在上一篇的文章的末尾,我已经提出了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/,第一时间看后续精彩文章。「防烂人备注**……&*¥」觉得好的话,顺手分享到朋友圈吧,感谢支持。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Machine Learning in Action
Peter Harrington / Manning Publications / 2012-4-19 / GBP 29.99
It's been said that data is the new "dirt"—the raw material from which and on which you build the structures of the modern world. And like dirt, data can seem like a limitless, undifferentiated mass. ......一起来看看 《Machine Learning in Action》 这本书的介绍吧!