《一个 bug 我们追了十几轮,前 7 次猜测全错,真凶是一行"用循环算乘法"》
我们用中文写了一个 x86-64 操作系统,从引导、长模式、文件系统一路写到自举编译器——编译器自己编译自己。
然后卡住了。
真机上一敲编译命令,屏幕就冻住。等 4 小时,不动。等更久,还是不动。
于是开始了长达十几轮的排查。我们的猜测名单:
缓存脏数据 ❌
LBA 扇区偏移 ❌
栈溢出 ❌
中断干扰 ❌
串口 busy-wait ❌
USB 传输无超时 ❌
读盘数据错 ❌
前 7 次,全部被证伪。
转机来自一个很朴素的观察:屏幕底部的进度色块其实一直在长,只是慢。
——不是死机,是龟速。
那为什么会慢到像死机?我们把编译器源码按段统计了一遍,结果让人沉默:
段 用乘法 用累加循环
词法 / 语法 10 处 4 处
代码生成 0 处 18 处
代码生成段,一处乘法都没用。
正确写法(词法段):
偏移 乘 24
同一个项目里的另一种写法(代码生成段):
当 (j 小于 i) {
偏移 加 32
j 加 1
}
用 O(n) 循环去算 i × 32。
于是符号表查找退化成 O(n²)——它被调用 26 处,遍布表达式生成热路径。再叠加真机的不可缓存内存区(惩罚约 27 倍),几分钟的任务膨胀成了几天。
最扎心的是这段代码的注释:
# 倍数 * 8 ← 注释自己写着"× 8"
当 (k 小于 倍数) { 偏移 加 8; k 加 1 }
注释知道该用乘法,代码没用。
改完 32 处后:
修复前:4 小时以上跑不完
修复后:10 分钟跑完
同一份代码在 QEMU 上:22.5 秒
教训不是"记得用乘法"。
而是:规范如果只写在文档里,它就等于不存在。
这条"用乘法别用累加"我们早就写下来了——然后被违反了 18 处。而那些做成了脚本、构建期强制检查的约束(地址校验、容量红线)?一次都没被违反过。
#操作系统 #编译器 #性能优化 #中文编程 #程序员
我们用中文写了一个 x86-64 操作系统,从引导、长模式、文件系统一路写到自举编译器——编译器自己编译自己。
然后卡住了。
真机上一敲编译命令,屏幕就冻住。等 4 小时,不动。等更久,还是不动。
于是开始了长达十几轮的排查。我们的猜测名单:
缓存脏数据 ❌
LBA 扇区偏移 ❌
栈溢出 ❌
中断干扰 ❌
串口 busy-wait ❌
USB 传输无超时 ❌
读盘数据错 ❌
前 7 次,全部被证伪。
转机来自一个很朴素的观察:屏幕底部的进度色块其实一直在长,只是慢。
——不是死机,是龟速。
那为什么会慢到像死机?我们把编译器源码按段统计了一遍,结果让人沉默:
段 用乘法 用累加循环
词法 / 语法 10 处 4 处
代码生成 0 处 18 处
代码生成段,一处乘法都没用。
正确写法(词法段):
偏移 乘 24
同一个项目里的另一种写法(代码生成段):
当 (j 小于 i) {
偏移 加 32
j 加 1
}
用 O(n) 循环去算 i × 32。
于是符号表查找退化成 O(n²)——它被调用 26 处,遍布表达式生成热路径。再叠加真机的不可缓存内存区(惩罚约 27 倍),几分钟的任务膨胀成了几天。
最扎心的是这段代码的注释:
# 倍数 * 8 ← 注释自己写着"× 8"
当 (k 小于 倍数) { 偏移 加 8; k 加 1 }
注释知道该用乘法,代码没用。
改完 32 处后:
修复前:4 小时以上跑不完
修复后:10 分钟跑完
同一份代码在 QEMU 上:22.5 秒
教训不是"记得用乘法"。
而是:规范如果只写在文档里,它就等于不存在。
这条"用乘法别用累加"我们早就写下来了——然后被违反了 18 处。而那些做成了脚本、构建期强制检查的约束(地址校验、容量红线)?一次都没被违反过。
#操作系统 #编译器 #性能优化 #中文编程 #程序员
展开
评论
2