Go 中的一十亿行挑战:从 1分45秒 到 4秒 的九种方案
为“1 Billion Row Challenge”优化 Go 代码——对一个包含 13GB、10亿行温度读数的文本文件进行解析——展示了通过仔细剖析性能、自定义数据结构和并行化,一个看似朴素的程序能被推到什么程度,从接近两分钟降到大约四秒。评论者将这些 Go 结果与高度调优的 Java、.NET、C++、Rust 以及基于数据库和 GPU 的方案进行比较,指出 JVM 和 .NET 的 JIT/AOT 工具链及 SIMD 支持即使在极端、非惯用代码下也能获得更低的运行时间。许多人强调,这类微基准测试虽然是探索性能极限的有趣方式,但真实世界的方案必须在原始速度、简洁性、健壮性、可移植性以及高级库和数据库的能力之间取得平衡。
语言性能对比
- 讨论集中在为什么最快的 Java 和 C#/.NET 方案会胜过最快的 Go 方案,有时甚至快 2–4 倍。
- 有人认为 Java 和 .NET 受益于非常激进的 JIT/AOT、SIMD intrinsic、PGO,以及数十年的运行时调优。
- 也有人反驳说,Go 原则上也可以使用类似的算法技巧;差距更多在于实现投入,而不是本质上不可能做到。
- 关于 JVM “经常能胜过 C”的说法也有争论;怀疑者认为这只在挑选过的案例中才成立。
- Rust、C++、Swift、Dart、Node.js,甚至 R 也通过其他排行榜或示例被提及,但由于硬件不同,对比并不清晰。
优化技巧和数据结构
- 关键技巧包括:自定义哈希表、内存映射 IO、仅用整数解析温度、循环展开,以及谨慎使用 unsafe 内存访问。
- 关于进一步提升的可能性也有讨论:栈分配数组、减少
copy()、对站点名称使用 eager trie、为温度建立查找表,以及完美哈希。 - 一些参与者怀疑大型 LUT 能否胜过简单算术,因为缓存和内存延迟成本可能更高。
IO、缓存与基准测试注意事项
- 许多人指出,重复运行会让 13GB 文件留在操作系统缓存或 RAM 磁盘中,因此磁盘带宽并不是瓶颈。
- 对 Java 方案似乎超过 SSD 吞吐量的疑问,则通过缓存和内存文件系统得到解释。
- 还有人指出,并行化能带来很大收益,但不能直接与
cat这类单线程工具相比较。
库、数据库、GPU 和高级工具
- 有人测试或提议使用 Polars、DuckDB、BigQuery、SQL 数据库,甚至 GPU;这些方案用更高层的代码也能达到几十秒或几分钟。
- 也有人认为,对于真实系统,把更多工作放在数据库里做在运营上可能更简单,而且性能也有竞争力,而不必手写代码。
现实相关性与 Go 的特定说明
- 几位评论者强调这个练习本质上是一个“游戏”:真实代码还需要健壮的解析和错误处理,这会以牺牲速度换取正确性。
- 其他人则关注 Go 编译器的局限(SIMD 支持较弱、PGO 适中、内联不够激进)与更成熟的 Java/.NET 工具链之间的差异,同时也指出 Go 在简单性和快速启动方面的优势。
- 通过 Go 的
pprof做分析,以及禁用 GC、锁定 OS 线程和使用 unsafe 指针等技巧,被认为很有用,但不太符合惯用写法。