从 105 秒到 3.4 秒:Go 程序如何极速处理 10 亿行数据
当面对 10 亿行、13GB 文本文件 时,你的第一反应可能是:"Go 能行吗?"
一、挑战背景
二、生成测试数据
在本地测试前,先生成一个大文件:
package main
运行后得到一个 measurements.txt,可以直接用于后续测试。
三、优化路线全解析
1️⃣ Baseline:最直接的写法(105 秒)
scanner := bufio.NewScanner(file)
-
特点:可读性极好,逻辑清晰。
-
问题:
- 每行会分配新的字符串;
strconv.ParseFloat 太通用,开销大;
map[string] 大量字符串 key 哈希。
-
耗时:约 105 秒。
2️⃣ 自定义温度解析函数(55 秒)
观察输入:温度都是 两位整数 + 一位小数,范围有限。我们没必要用通用的浮点解析器(strconv.ParseFloat),可以直接按字节解析:
func parseTemp(b []byte) int {
// 自定义解析逻辑
}
- 原因:避免
strconv.ParseFloat 的复杂逻辑,只保留实际需要的部分。
- 效果:耗时减半 → 55 秒。
3️⃣ 批量读取替代 Scanner(41 秒)
bufio.Scanner 每次调用会有切片和边界处理开销。改用大 buffer,自己按 \n 分割:
buf := make([]byte, 1<<20) // 1MB
- 原因:减少函数调用与切片分配。
- 效果:41 秒,比 baseline 提升 2.5 倍。
4️⃣ 自定义哈希表(22 秒)
标准库的 map[string] 要做字符串哈希、分配内存,代价高。我们实现自己的哈希表,直接用 []byte 存 key:
type entry struct {
// 自定义哈希表结构
}
- 原因:减少字符串分配与 GC 压力。
- 效果:进一步减半 → 22 秒。
5️⃣ 并行化(3.4 秒)
最后一招:利用多核。将文件切分成多个 chunk,多个 goroutine 并行处理,最后合并结果。
numCPU := runtime.NumCPU()
- 原因:现代 CPU 都是多核,单线程浪费硬件。
- 效果:耗时直降至 3.4 秒,比 baseline 快 30 倍。
四、最终完整代码
下面是整合所有优化的完整程序(含并行 + 自定义哈希表 + 手写解析):
package main
五、实战效果
在我的机器上:
- Baseline:105 秒
- 最终优化版:3.4 秒
性能提升近 30 倍!
六、总结
-
正确性优先:先写 baseline,保证结果对。
-
找到瓶颈:字符串分配、解析函数、map 操作、I/O。
-
逐步优化:避免不必要的分配、写定制解析函数、批量读取。
-
敢于突破标准库:自定义数据结构能带来数量级提升。
-
充分利用多核:并行化是最后的加速神器。
这就是 Go 的魅力:你既能快速写出清晰可读的代码,又能在需要时挖掘出接近底层的性能潜力。
参考资料
One Billion Row Challenge (1BRC)
访问地址:https://benhoyt.com/writings/go-1brc/