64KB 数据,256KB 指令。

Raymond Chen 在 The Old New Thing 里讲了一个 Windows 旧故事:Windows 曾在某些非 x86 处理器上提供 x86-32 程序模拟。具体是哪种处理器,原文没说。这里不能替它补设定。

真正反常的是这段代码:一个程序要在栈上分配并初始化约 64KB 缓冲区。正常做法很短。某个编译器却把清零循环完全展开,生成 65536 条“写一个字节到内存”的指令。每条 4 字节,合计约 256KB 代码。

模拟器团队最后做了一件很脏、也很现实的事:在二进制翻译器里识别这段 horrible function,把它替换成等价的紧凑循环。

离谱点不在清零,而在把烂代码交给模拟器吞

这套模拟器不是逐条解释执行。它用的是 binary translation。粗略说,就是把 x86-32 当成一种字节码,再 JIT 成目标处理器的原生代码。

这类系统怕的不是一条慢指令,而是大块低质量代码被翻译、缓存、执行。256KB 的重复写字节指令,会拖累翻译成本,也会污染指令缓存。它不是安全漏洞,也不是系统级故障。原文说得很克制:代码质量很糟,翻译器为它做了特殊优化。

问题正常做法这段代码
栈空间先做 stack probe仍要处理大栈空间
栈指针调整一次不是主要问题
初始化小而紧的循环展开成 65536 条写字节指令
代码体积很小约 256KB
翻译器正常翻译加特判,替换成紧凑循环

循环展开本来是优化手段。问题是尺度失控。64KB 清零写成 256KB 指令,像为了关一盏灯跑完整栋楼的开关。

这里也要划边界:这不是现代 ARM64 Windows 的直接案例,也不能拿来类比 Rosetta 的具体实现。原文没有给这个证据。能确定的只有一件事:Windows 兼容性工程里,确实出现过一次“模拟器替坏编译结果擦屁股”。

受影响的不是普通用户,而是做底层和迁移的人

普通用户大概率只会看到一个结果:旧程序能不能跑,跑得卡不卡。至于背后是编译器犯蠢,还是模拟器兜底,用户不关心。

真正该看这件事的,是两类人。

做底层系统、虚拟化、模拟器、兼容层的人,要把它当成预算提醒。兼容性不是写完指令翻译就结束。你迟早会遇到奇怪编译器、旧工具链、手写汇编、未定义但被依赖的行为。最后它们会变成特判、shim、黑名单、白名单,塞进系统深处。

做性能工程和工具链迁移的开发团队,也该多看一眼生成代码。尤其是大栈对象初始化、热路径清零、跨架构迁移场景。不要默认“编译器总会做对”。遇到老工具链,至少抽样看汇编或反汇编;关键路径要用 profile 说话,而不是靠信仰。

动作很具体:如果团队还在维护老 x86-32 程序,并计划跑在兼容层或模拟器上,迁移前别只测功能。要测启动、初始化、热路径和生成代码体积。发现这种展开怪物,优先改源码或换工具链,而不是指望平台永远替你兜底。

对平台维护者也一样。该观察的变量不是“有没有天才工程师救场”,而是特判数量会不会失控:

变量说明风险
特判是否只服务单个函数越具体,越像救火维护成本难复用
是否影响通用翻译规则越通用,越要谨慎验证可能误伤其他程序
是否有退出条件旧程序消失后能否删除兼容层越堆越厚

这也是兼容性工程最难受的地方。修一个点很爽。背一个规则很久。

兼容性是护城河,也是利息表

我不太买账“这只是微软工程师机智”的讲法。机智当然有,但那只是表层。

这件事更像 Windows 长期路线的一张小切片:平台承诺旧程序继续跑,用户留下来,企业少迁移,生态更稳。代价沉到系统内部,由兼容层、翻译器、运行时规则慢慢消化。

Windows 的强大,确实有一部分来自这种执拗。很多企业软件、行业软件、老工具链,靠的就是平台愿意忍。这个判断要给足。

但别把忍耐讲成纯美德。兼容性也是一张利息表。上游写下的烂代码,平台今天替它跑通,明天就可能要替它维护边界。

历史上很多平台战争都绕不开这个账。PC 生态能滚大雪球,靠的不是每个程序都优雅,而是足够多的旧东西还能继续用。不完全一样,但权力结构相似:谁控制平台,谁就有动力把历史包袱包装成用户连续性。

“积羽沉舟。”单个兼容补丁看起来很轻。特判多了,系统就开始背着过去走路。

这次模拟器团队修的是性能问题,也是生态压力。一个糟糕函数,本该由编译器负责。可代码到了用户机器上,平台就成了最后兜底的人。

所以这段旧事真正有用的提醒很简单:看一个平台的技术能力,别只看发布会功能。也要看它愿意替多少旧债付利息,以及它有没有能力不被这些利息拖垮。

代码可以荒唐,历史不能重启。兼容性工程最硬的部分,就是把荒唐限制在可控范围内。