Go 从 1.0 到 1.22 的性能表现
两年前,我在 Go 1.2 到 1.18 的所有版本上对比了我的 GoAWK 解释器的两项不同基准测试。
在本文中,我将重新运行这些基准测试,补上之前缺失的 Go 版本(1.0 和 1.1),并加入新版本(1.19 到 1.22)。我还会加入启用配置文件引导优化(PGO)后的结果,该功能是在 Go 1.20 中引入的。文中会大量引用原文内容,这样你就不必为了了解测试设置而重读旧文了。
Go 程序变快的原因有很多:Go 团队和外部贡献者改进了编译器,并优化了运行时、垃圾回收器和标准库。在这里,我们对比了使用从 1.0 到 1.22 各个已发布 Go 版本编译 GoAWK 时的性能——1.22 是撰写本文时的最新版本。
我通过在 GoAWK 上运行两个 AWK 程序来完成测试,它们代表了 AWK 两种截然不同的典型用途:带字符串处理的 I/O 和数值计算。
首先是 countwords,这是一个字符串处理任务,用于统计输入中各个单词的出现频率并打印出单词及其计数。这是 AWK 脚本的典型应用。输入是《钦定版圣经》文本重复拼接 10 次后的版本(我之前做性能对比时也用过它)。代码如下:
{
for (i=1; i<=NF; i++)
counts[tolower($i)]++
}
END {
for (k in counts)
print k, counts[k]
}第二个程序是 sumloop,一个紧凑的循环,会将循环计数器反复累加到一个变量上。这并非 AWK 的典型用法,但很适合用来测试 GoAWK 字节码解释器循环的性能:
BEGIN {
for (i=0; i<10000000; i++)
sum += i+i+i+i+i
}为了让 GoAWK 能在旧版 Go 上编译通过,我对其代码做了少量调整。特别是在 Go 1.0 上,因为它还没有bufio.Scanner,而 GoAWK 大量使用了它。我为 1.0 移植了 Go 1.1 中 bufio.Scanner 的实现。
图表中的耗时数字是在我那台 x86-64 Linux 笔记本上测得的秒数(取三次运行中的最佳成绩)。蓝线代表 countwords,红线代表 sumloop(顺带一提,上次我把结果的标签标错了)。请注意,这次 Y 轴采用了对数刻度,以便更清晰地观察近年来那些较为细微的性能提升。
图表中还包含了每个 Go 版本编译出的 GoAWK 二进制文件大小——即那条浅灰色的线。
和上次一样,我用一个 Python 脚本来批量运行并测量耗时。图表如下(如果你更喜欢表格形式也可以查看):

最大的性能提升出现在 1.3、1.5、1.7 和 1.12 这几个版本中。此后,速度的提升就变得非常平缓了——所有容易摘的果子早已被摘完。
这次 countwords 在 Go 1.2 上出现了一个奇怪的性能回退:耗时从 1.1 的 7.5 秒飙升到 1.2 的 25.5 秒(!),然后在 1.3 又降到了 2.8 秒。这几乎可以肯定是由栈“热分裂”问题引起的,该问题在 Go 团队将“goroutine 栈的实现从旧的‘分段’模型改为连续模型”后在 1.3 中得到修复。
我通过性能分析找到了 1.2 异常的原因,发现运行时栈操作占据了运行时间的一大部分。以下是 pprof 输出的前几行:
$ go tool pprof --text ./goawk_1.2 go12.prof
Total: 1830 samples
332 18.1% 18.1% 332 18.1% runtime.newstack
296 16.2% 34.3% 296 16.2% runtime.memclr
281 15.4% 49.7% 281 15.4% runtime.oldstack
222 12.1% 61.8% 619 33.8% github.com/benhoyt/goawk/interp.(*interp).execute
91 5.0% 66.8% 91 5.0% runtime.lessstack
75 4.1% 70.9% 133 7.3% github.com/benhoyt/goawk/interp.(*interp).callBuiltin
57 3.1% 74.0% 57 3.1% runtime.stackfree
53 2.9% 76.9% 81 4.4% strings.FieldsFunc
...PGO 仅能带来几个百分点的性能提升,在 Go 1.22 下,countwords 约提升 2%,sumloop 约提升 7%。我发布的 GoAWK 二进制文件都是用 PGO 编译的。
多年来,二进制文件的大小一直保持得相当稳定,除了在 1.2 版本有一次大幅增长。即使启用 PGO,二进制文件也只增大了约 5%,所以我认为通常还是值得的。
总体而言,如今 countwords 的速度约为使用 Go 1.0 时的 8 倍,而 sumloop 则快了约 24 倍。感谢 Go 团队多年来付出的辛勤努力!
随机一篇博客
评论
登录后参与讨论