本文100%由本人(Haoxiang Ma)原创,如需转载请注明出处。
本文写于2019/02/16,基于
Go 1.11。
至于其他版本的Go SDK,如有出入请自行查阅其他资料。
写本文的动机来源于Golang中文社区里一篇有头没尾的帖子《Go语言字符串高效拼接》,里面只提了Golang里面字符串拼接的几种方式,但是在最后却不讲每种方式的性能,也没有给出任何的best practice。本着无聊 + 好奇心,就决定自行写benchmark来测试,再对结果和源码进行分析,试图给出我认为的best practice吧。
根据帖子里的内容,在Golang里有5种字符串拼接的方式:
直接+号拼接
|
fmt.Sprint()拼接
// fmt拼接 |
strings.Join()拼接
// strings.Join拼接 |
Buffer拼接
// bytes.Buffer拼接 |
Builder拼接
// strings.Builder拼接 |
为了测试各自的性能,就用Golang自带test模块的benchmark来进行测试。
在测试中,分3组数据,5组测试,即一共3 * 5 = 15次独立测试。其中3组数据是指:
"hello""hello""hello"5组测试是指:
Benchmark代码如下:
package main |
经过测试(go test -bench=. -benchmem),结果如下:
...... |
可以看出
速度&内存分配都很优秀的strings.Join()
func Join(a []string, sep string) string { |
可以看出strings.Join()为什么表现如此优秀,主要原因是只有1次的显式内存分配(b := make([]byte, n))和1次隐式内存分配(return string(b)),不需要在拼接过程中反复多次分配内存,挪动内存里的数据,减少了很多内存管理的消耗。
略差一筹的bytes.Buffer.WriteString()
// 尝试扩容n个单位 |
为什么bytes.Buffer.WriteString()性能比Join差呢,其实也是内存分配策略惹的祸。在Join里只有两次内存空间申请的操作,而Buffer里可能会有很多次。具体来说就是buf := makeSlice(2*c + n)这一句,每次重申请只申请2 * c + n的空间,用完了就要再申请2 * c + n。当拼接的数据项很多,每次申请的空间也就2 * c + n,很快就用完了,又要再重新申请,所以造成了性能不是很高。
略差一筹的strings.Builder()
func (b *Builder) WriteString(s string) (int, error) { |
代码很简洁,就是最直白的slice append,一时append一时爽,一直append一直爽。所以当底层slice的可用空间不足,就会在append里一直申请新的内存空间。跟bytes.Buffer不同的是,这里并没有自己管理“扩容”的逻辑,而是交由原生的append函数去管理。
最差劲的fmt.Sprint()
type buffer []byte |
printer里的核心数据结构就是buf,而buf其实就是一个[]byte,所以给buf不停地拼接字符串,空间不够了又继续开辟新的内存空间,所以性能低下。
实际上,只有当拼接的字符串非常非常多的时候,才需要纠结性能。像本文里动辄拼接10K、50K、100K个字符串的情况在实际业务中应该是很少很少的。
如果实在要纠结性能,参考以下几点
原文:https://www.cnblogs.com/lijianming180/p/12099473.html