Go语言字符串高效拼接(三)

栏目: Go · 发布时间: 6年前

内容简介:在上一篇关于字符串拼接的文章在上一篇的结尾中,我留下悬念说其实还有优化的空间,这就是今天这篇文章,字符串拼接系列的第三篇,也是字符串拼接的最后一篇产生的原因,今天我们就看下如何再提升既然要优化

在上一篇关于字符串拼接的文章 Go语言字符串高效拼接(二) 中,我们终于为 Builder 拼接正名了,果真不负众望,尤其是拼接的字符串越来越多时,其性能的优越性更加明显。

在上一篇的结尾中,我留下悬念说其实还有优化的空间,这就是今天这篇文章,字符串拼接系列的第三篇,也是字符串拼接的最后一篇产生的原因,今天我们就看下如何再提升 Builder 的性能。关于第一篇字符串高效拼接的文章可点击 Go语言字符串高效拼接(一) 查看。

Builder 慢在哪

既然要优化 Builder 拼接,那么我们起码知道他慢在哪,我们继续使用我们上篇文章的测试用例,运行看下性能。

Builder10-8     5000000     258 ns/op       480 B/op        4 allocs/op
Builder100-8    1000000     2012 ns/op      6752 B/op       8 allocs/op
Builder1000-8   100000      21016 ns/op     96224 B/op      16 allocs/op
Builder10000-8  10000       195098 ns/op    1120226 B/op    25 allocs/op
复制代码

针对既然要优化 Builder 拼接,采取了10、100、1000、10000四种不同数量的字符串进行拼接测试。我们发现每次操作都有不同次数的内存分配,内存分配越多,越慢,如果引起GC,就更慢了,首先我们先优化这个,减少内存分配的次数。

内存分配优化

通过cpuprofile,查看生成的火焰图可以得知, runtime.growslice 函数会被频繁的调用,并且时间占比也比较长。我们查看 Builder.WriteString 的源代码:

func (b *Builder) WriteString(s string) (int, error) {
	b.copyCheck()
	b.buf = append(b.buf, s...)
	return len(s), nil
}
复制代码

可以肯定是 append 方法触发了 runtime.growslice ,因为 b.buf 的容量 cap 不足,所以需要调用 runtime.growslice 扩充 b.buf 的容量,然后才可以追加新的元素 s... 。扩容容量自然会涉及到内存的分配,而且追加的内容越多,内容分配的次数越多,这和我们上面性能测试的数据是一样的。

既然问题的原因找到了,那么我们就可以优化了,核心手段就是减少 runtime.growslice 调用,甚至不调用。照着这个思路的话,我们就要提前为 b.buf 分配好容量 cap 。幸好 Builder 为我们提供了扩充容量的方法 Grow ,我们在进行 WriteString 之前,先通过 Grow 方法,扩充好容量即可。

现在开始改造我们的 StringBuilder 函数。

//blog:www.flysnow.org
//微信公众号:flysnow_org
func StringBuilder(p []string,cap int) string {
	var b strings.Builder
	l:=len(p)
	b.Grow(cap)
	for i:=0;i<l;i++{
		b.WriteString(p[i])
	}
	return b.String()
}
复制代码

增加一个参数 cap ,让使用者告诉我们需要的容量大小。 Grow 方法的实现非常简单,就是一个通过 make 函数,扩充 b.buf 大小,然后再拷贝 b.buf 的过程。

func (b *Builder) grow(n int) {
	buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
	copy(buf, b.buf)
	b.buf = buf
}
复制代码

那么现在我们的性能测试用例变成如下:

func BenchmarkStringBuilder10(b *testing.B) {
	p:= initStrings(10)
	cap:=10*len(BLOG)
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		StringBuilder(p,cap)
	}
}

func BenchmarkStringBuilder1000(b *testing.B) {
	p:= initStrings(1000)
	cap:=1000*len(BLOG)
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		StringBuilder(p,cap)
	}
}

复制代码

为了说明情况和简短代码,这里只有10和1000个元素的用例,其他类似。为了把性能优化到极致,我一次性把需要的容量分配足够。现在我们再运行性能(Benchmark)测试代码。

Builder10-8     10000000    123 ns/op       352 B/op    1 allocs/op
Builder100-8    2000000     898 ns/op       2688 B/op   1 allocs/op
Builder1000-8   200000      7729 ns/op      24576 B/op  1 allocs/op
Builder10000-8  20000       78678 ns/op     237568 B/op 1 allocs/op
复制代码

性能足足翻了1倍多,只有1次内存分配,每次操作占用的内存也减少了一半多,降低了GC。

小结

这次优化,到了这里,算是结束了,写出来后,大家也会觉得不难,其背后的原理也非常情况,就是预先分配内存,减少 append 过程中的内存重新分配和数据拷贝,这样我们就可以提升很多的性能。所以对于可以预见的长度的切,都可以提前申请申请好内存。

字符串拼接的系列,到这里结束了,一共三个系列,希望对大家所有帮助。

本文为原创文章,转载注明出处,「总有烂人抓取文章的时候还去掉我的原创说明」欢迎扫码关注公众号 flysnow_org 或者网站www.flysnow.org/,第一时间看后续精彩文章。「防烂人备注**……&*¥」觉得好的话,顺手分享到朋友圈吧,感谢支持。

Go语言字符串高效拼接(三)

以上所述就是小编给大家介绍的《Go语言字符串高效拼接(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

浅薄

浅薄

[美]尼古拉斯·卡尔 / 刘纯毅 / 中信出版社 / 2015-11 / 49.00 元

互联网时代的飞速发展带来了各行各业效率的提升和生活的便利,但卡尔指出,当我们每天在翻看手机上的社交平台,阅读那些看似有趣和有深度的文章时,在我们尽情享受互联网慷慨施舍的过程中,我们正在渐渐丧失深度阅读和深度思考的能力。 互联网鼓励我们蜻蜓点水般地从多种信息来源中广泛采集碎片化的信息,其伦理规范就是工业主义,这是一套速度至上、效率至上的伦理,也是一套产量最优化、消费最优化的伦理——如此说来,互......一起来看看 《浅薄》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换