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

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

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

在上一篇关于字符串拼接的文章 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语言字符串高效拼接(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

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

Beginning XSLT 2.0

Beginning XSLT 2.0

Jeni Tennison / Apress / 2005-07-22 / USD 49.99

This is an updated revision of Tennison's "Beginning XSLT", updated for the new revision of the XSLT standard. XSLT is a technology used to transform an XML document with one structure into another ......一起来看看 《Beginning XSLT 2.0》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具