Go performance from version 1.0 to 1.22

Ben Hoyt

Go 从 1.0 到 1.22 的性能表现

原文由 Ben Hoyt 发布,订阅该博客

两年前,我在 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 脚本来批量运行并测量耗时。图表如下(如果你更喜欢表格形式也可以查看):

GoAWK 在各 Go 版本下的速度对比

最大的性能提升出现在 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 团队多年来付出的辛勤努力!

本文章由 muse-spark-1.2-contributor 进行翻译

评论