为什么一个额外的构建步骤会使我的 Zig 应用程序快 10 倍?

过去几个月,作者对 Zig 编程语言和以太坊加密货币这两项技术感到好奇,并用 Zig 编写了以太坊虚拟机的字节码解释器。Zig 擅长性能优化,能精细控制内存和控制流,作者用它来与官方 Go 实现的以太坊进行基准测试,开始时 Zig 实现的性能比 Go 实现差约 40%。
后来作者对基准测试脚本进行简单重构后,应用性能下降,通过比较zig build run和直接运行编译后的二进制文件,发现额外的构建步骤使程序运行快了近 10 倍,这让作者很困惑。
为调试性能之谜,作者简化应用为只计算从 stdin 读取的字节数的程序,仍能看到性能差异,zig build run只需 13 微秒,而直接运行编译后的二进制文件需 162 微秒。作者的测试是一个包含echoxxdzig build run的 bash 管道,差异在于zig build run会重新编译应用,而直接运行编译后的二进制文件则不会。
作者在 Ziggit 论坛发帖询问,起初得到的回复说有“输入缓冲”问题但没有具体建议,Zig 的创始人 Andrew Kelly 指出作者犯了另一个性能错误,即每次读取字节都进行一次系统调用。后来作者的朋友 Andrew Ayer 解释了原因,是因为 bash 管道中的命令是并行执行的,zig build run在编译时xxd已经开始,所以count-bytes开始时输入已准备好,而直接运行编译后的二进制文件时count-bytes要等待xxd填充管道缓冲区。
作者意识到自己对 bash 管道的思维模型是错误的,通过写简单的 bash 脚本证明了管道中的命令是同时开始的。理解这一点后,作者对字节计数器的行为有了合理的解释。
作者通过将准备阶段和执行阶段分开,使基准测试从 438 微秒下降到 67 微秒。按照 Andrew Kelly 的建议使用缓冲读取器,性能又提高了 16%。对更大输入的基准测试表明,缓冲输入读取使 Zig 实现比官方以太坊实现快约 2 倍。作者还通过使用固定缓冲区分配器作弊,在知道最大内存需求的情况下,使以太坊实现比官方 Go 实现快 3 倍。
作者的经验是要尽早经常进行性能基准测试,将基准测试纳入持续集成并存档结果,便于识别测量变化的原因,同时要理解自己的指标,避免因忽略某些因素而导致错误。源代码为eth-zvm,是用 Zig 实现的业余以太坊虚拟机。

阅读 9
0 条评论