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

在上一篇关于字符串拼接的文章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过程中的内存重新分配和数据拷贝,这样我们就可以提升很多的性能。所以对于可以预见的长度的切,都可以提前申请申请好内存。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小樱的经验随笔

汇编语言第三版答案(王爽)

汇编语言答案(王爽)  此文只是用来存个档,不喜勿喷 检测点1.1 (1)1个CPU的寻址能力为8KB,那么它的地址总线的宽度为 13位。 (2)1KB的存储器...

43811
来自专栏流媒体

Android性能优化(TraceView使用)

看到第一行数据0(topLevel),topLevel包含整个trace周期。Incl Cup Time% 为100% 实际消耗cpu时间为1539.420...

743
来自专栏黑泽君的专栏

day58_BOS项目_10

之前的请假流程,是没有实际意义的,我们要使得我们流程变得有意义(有实际意义),需要在流程向下推进的过程中带着数据推进才有意义。如下图所示:

904
来自专栏Java成神之路

【转】 Java 多线程之一

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括...

1093
来自专栏腾讯IVWEB团队的专栏

通过ffi在Node.js中调用动态链接库(.so/.dll文件)

由于公司许多公共的后台服务已经有了非常成熟的C/C++编写的API,以供应用程序调用,node.js作为在公司内新兴的后台runtime在调用这些公共服务的时候...

1.1K0
来自专栏数据库

Redis列表的“绝地反击”

大家晚上好,今天介绍Redis中的列表数据结构。 Redis中的列表是用来存储多个有序的字符串的,最神奇的地方是:竟然可以在列表两端插入(push)和弹出(po...

22410
来自专栏大内老A

认识ASP.NET MVC的5种AuthorizationFilter

在总体介绍了筛选器及其提供机制(《深入探讨ASP.NET MVC的筛选器》)之后,我们按照执行的先后顺序对四种不同的筛选器进行单独介绍,首先来介绍最先执行的Au...

2416
来自专栏顶级程序员

为什么文件名要小写?

来自:阮一峰的网络日志 链接:www.ruanyifeng.com/blog/2017/02/filename-should-be-lowercase.htm...

2885
来自专栏阿杜的世界

使用SA分析内存溢出问题背景例子程序方式方法实践参考资料

在阅读《Java性能调优指南》一书的最后,书中介绍了Serviceability Agent,并给出了一些排查问题的示例,我感觉看书不够深刻,因此自己在macO...

1402
来自专栏阮一峰的网络日志

为什么文件名要小写?

上周,《中文技术文档写作规范》加入了文件的命名规则。 "文件名建议只使用小写字母,不使用大写字母。" "为了醒目,某些说明文件的文件名,可以使用大写字母,比...

2866

扫码关注云+社区

领取腾讯云代金券