从ADB到Quake引擎:90年代游戏引擎如何教你极致优化

从 Quake 汇编优化到 ADB 重写,理解底层机器比信任抽象层更重要。
本文基于 Android Developers Backstage 播客中 Fabien Sanglard 的分享,串联起 90 年代游戏引擎优化与当代 Android 工具链重构的共通经验。文章首先回顾 ADB Wi-Fi 的修复历程:团队用 2000 行 Rust 手写 MDNS,替换了约 90 万行的 Open Screen + Abseil 依赖,使用率从 7% 提升到 20%。随后借 Quake 引擎中 Michael Abrash 的汇编级优化案例,说明编译器受语言规范约束无法复现某些手动优化,理解领域语义才能突破抽象层。文章还披露 ADB 内部的历史遗留问题,并用 ASAN 捕获了一个字节的越界写入。最后讨论了 LLM 在编程与调试中的边界:适合生成"石膏板代码",但核心组件仍需逐行理解。
从ADB到Quake引擎:90年代游戏引擎如何教你极致优化
一次午餐引出的思考
Android Developers Backstage 播客最近一期节目中,Android Studio 与 ADB 团队的核心成员 Fabien Sanglard 分享了一段特别的经历。在 QuakeCon 上,他与 John Carmack、John Romero、Tom Hall、Adrian Carmack 等传奇开发者共进午餐,聊到了 Michael Abrash 对 Quake 引擎做的汇编级优化——他一个人就把 Quake 的帧率翻了一倍。如果没有这次优化,Quake 可能根本不会发售。
这场对话让 Fabien 重新审视了自己正在做的 ADB 重构工作。30年前的极限优化思路,与今天 Android 工具链面临的工程问题,竟然有着惊人的相似之处。
据 Hacker News 上编译器领域专家的讨论,即使在今天,现代编译器仍然无法复现 Abrash 当年手动实现的优化水平。这引出一个关键问题:当抽象层越来越厚,我们是否丢失了对底层机器的理解?
ADB Wi-Fi 的漫长修复之路
为什么 ADB Wi-Fi 曾经体验糟糕
ADB Wi-Fi 的使用率从 7%-8% 跃升到 20%,这背后是 Fabien 团队对整个技术栈的彻底修复。问题出在 MDNS(多播 DNS)的各个环节:设备端使用的是十多年未更新的 Apple Bonjour 参考实现,以 root 权限运行且存在内存泄漏;工作站端则混用了两个不同的库——Mac 上用的是从手机端移植的 Bonjour,Linux 和 Windows 上用的是来自 Chromecast 开源项目 Open Screen 的实现。
但 Open Screen 是为 Chromecast 设计的:设备永远开机、永远在线,所以它压根没有实现主动探测网络的功能。结果就是:你合上笔记本去开会,回来重新打开,ADB 就找不到手机了——因为没人发送探测包,手机也不会主动广播自己。

MDNS(多播 DNS) 是一种零配置网络协议,允许设备在不依赖中心 DNS 服务器的前提下,通过多播地址(IPv4 下为 224.0.0.251:5353)在局域网内相互发现服务。每台设备既可以主动发送查询("谁是 _adb._tcp.local?"),也可以主动广播自己的服务信息("我是 adb-xxx._adb._tcp.local,端口 5555")。ADB Wi-Fi 正是靠它让工作站自动找到手机。问题在于,MDNS 的发现是"事件驱动"的:当网络拓扑变化(如笔记本休眠后重新接入)时,必须有人发送新的探测包或响应包,才能刷新缓存。Open Screen 是为常年在线、网络稳定的 Chromecast 场景设计的,省略了这套主动探测逻辑,因此一旦网络状态改变,工作站就再也找不到设备。
用2000行代码替换百万行依赖
修复方案令人震撼。Fabien 团队在设备端将 MDNS 注册重定向到 Wi-Fi 团队自研的 SD Manager(基于 Java),彻底解决了重复注册和内存泄漏问题。在工作站端,Fabien 做了一个大胆的决定:直接用 Rust 手写一个 MDNS 实现。
结果对比鲜明——Open Screen 加上其依赖的 Abseil 库总计约 90 万行代码,而新的 Rust 实现只有 2000 行。据 Fabien 在播客中介绍,因为 UDP 多播允许共享套接字,他们可以用一个统一的实现覆盖所有平台,不再需要为 Mac 单独维护一套。
这里有一个值得开发者深思的经验:复用代码是好事,但你必须理解你在复用的是什么。Fabien 把库依赖比作信用卡——有时你时间紧迫,不得不刷,但关键是最终要还清。要么重写你真正需要的那一小部分,要么持续更新不要 fork。
编译器做不到的事:Quake 的启示
为什么手动优化仍有价值
回到 Quake 的话题。Abrash 当年在汇编层面做的优化,让帧率直接翻倍,这不是靠更好的硬件或编译器就能解决的。Fabien 在播客中指出,现代编译器受限于语言规范定义的各种语义约束,无法做出某些激进优化。但当你深入理解自己的应用领域,知道哪些语义约束在你的场景下不适用时,你就能做出编译器做不到的优化。
他在自己的 Kotlin Explorer 工具中观察到一个典型例子:在 Kotlin 中把 companion object 当作静态成员使用,但实际调用时 VM 必须验证对象是否正确初始化、是否需要触发初始化流程——这些检查大多数时候毫无必要,但编译器不能省略,因为语言规范要求如此。
编译器受语言规范约束而无法优化,根源在于"可观察行为"(observable behavior)这一概念。以 C/C++ 为例,标准规定了某些操作顺序、初始化时机、内存可见性必须按语义执行,即使程序员知道在特定场景下可以省略。比如 Kotlin/JVM 中,companion object 的初始化必须满足类加载时的线程安全与惰性语义,VM 因此必须在每次访问时插入初始化状态检查。编译器不能"聪明地"跳过这些检查,因为一旦跳过,在另一段代码里就可能观察到违反规范的结果。Abrash 当年在汇编层可以手动跳过这些检查,是因为他掌握着 Quake 运行时的全部上下文,知道哪些约束在本场景下永远不会被触发。这种"领域知识驱动的越界优化",是通用编译器在原理上难以复制的。
旧技术的新生命
Fabien 提到一个有趣的例子:阴影贴图(shadow map)这项技术——通过从另一个视角渲染场景并存储结果来生成阴影——在90年代的引擎中就已经出现,但当时太昂贵无法实用。如今,它已经成为所有现代游戏的标配。
另一个引人注目的对比是:据播客中提到的一则消息,某个流行操作系统上的天气应用竟然占用 600MB 到 1.6GB 内存。当软件越做越臃肿时,90年代那些在极其有限的资源下精打细算的编程技巧,反而显得更有现实意义。

ADB 内部:从“石膏板”到“承重墙”
硬件抽象层的债务
ADB 的内部实现充满了历史遗留问题。Fabien 发现了一段两个线程通过管道通信的代码,深入研究后发现——竟然是同一个线程在跟自己说话。他问同事能不能直接删掉,对方说“你试试看”。类似的问题还有:ADB 每秒扫描整个 USB 设备树来检测设备连接,而标准做法应该是用 Netlink 接收内核事件通知。
更极端的是 Windows 平台上的 USB 驱动:由于二十年前的构建系统不支持 Windows,开发者在自己机器上编译了一个 32 位 x86 静态库,直接放入源码树中。这个预编译的库至今仍在被链接使用,而原始源码早已无法重新构建。
好在 ADB 现在能用 LLM 辅助重写这个 USB 驱动。Fabien 提出了一个重要的概念区分:“石膏板代码”和“承重墙代码”。一次性实验或快速原型用 LLM 生成没问题,就像石膏板一样随便用;但支撑系统的核心组件必须是承重墙,需要逐行理解。
Netlink 是 Linux 内核提供的一种用户态与内核态之间的双向通信机制,广泛用于网络配置、路由表变更、设备热插拔等事件通知。相比 ADB 目前每秒轮询整棵 USB 设备树的做法,Netlink 的 NETLINK_KOBJECT_UEVENT 套接字能主动推送内核检测到的设备接入/移除事件,延迟更低、CPU 占用几乎为零。轮询方案的问题不仅是浪费资源,还会在设备数量多时产生可观的系统调用开销,并且存在最长一秒的检测延迟。这类"用轮询代替事件通知"的代码在历史遗留系统中非常常见,往往是因为当年的开发环境不支持更现代的 IPC 机制,或者开发者当时不了解内核提供的接口。
一个字节的代价
Fabien 用 ASAN(AddressSanitizer)编译 ADB 后,仅运行一分钟就崩溃了——每次从网络接收数据包都有一字节的缓冲区越界写入。这类内存在 C/C++ 项目中极其隐蔽,但在 ASAN 的检测下无处遁形。修复之后,他做的第一件事就是重写 MDNS 相关模块。
ASAN(AddressSanitizer) 是 Google 开发的一种编译期插桩工具,能在运行时检测缓冲区溢出、释放后使用(use-after-free)、栈溢出等内存错误。它的原理是给每个内存分配区域周围添加"红区"(redzone),并维护影子内存来记录哪些字节可访问。当程序读写内存时,插桩代码会先查询影子内存,一旦越界立即报错并打印完整调用栈。对于 C/C++ 项目,ASAN 的额外内存开销约 2-3 倍,性能开销约 2 倍,但相比它能在测试阶段捕获的隐蔽 Bug,这个代价通常非常值得。文中提到的"每次接受数据包都有一字节越界写入",正是 ASAN 最擅长发现的类型:错误本身未必立即崩溃,但长期运行会悄悄破坏堆内存布局,导致难以复现的随机崩溃。
调试器与 LLM:尚未找到的契合点
Android 调试器技术栈横跨 Android Studio(GDI)、设备端 Agent(通过 JVMDI 与 ART 通信)和 ART 运行时。Fabien 团队正在用 JetBrains 的 10,000 多个调试器测试用例来验证 ART 上的表现,逐一定位差异并修复。
一个典型 Bug 很有启发性:在表达式中创建临时对象后,ART 的垃圾回收过于激进,在对象被使用之前就将其回收了。另一个问题是 ART 使用了不同实现的哈希表,导致类的创建顺序与 JVM 不一致。这些问题本质上都源于调试器依赖了 JVM 未文档化的行为。
有意思的是,Fabien 坦承他们在尝试将 LLM 与调试器结合时遇到了瓶颈。LLM 总是倾向于直接读取源代码——即使你故意只给它一个 JAR 包,它也会先反编译再获取源码。目前还没找到一个 LLM 配合调试器明显优于 LLM 直接读代码的场景。不过 Romain 提出了一个可能的方向:当 Bug 出在无法修改的第三方库中,或者需要节省 Token 时,调试器的精确信息获取能力可能成为优势。

写在最后
Fabien 目前正在写他的第三本书——关于 Quake 引擎的解析。前两本分别深入剖析了 Wolfenstein 3D 和 Doom 的引擎实现,但不仅限于游戏本身——书中还涉及 DOS 系统、386/486 架构、VGA 图形等底层知识。他解释浮点数工作原理的那一章,被 Romain 称为“读过的最好的浮点数解释”。
从 Quake 到 ADB,跨越三十年的工程实践指向同一个道理:理解你运行代码的机器,而不是盲目信任抽象层。无论是汇编级优化、MDNS 协议实现,还是 USB 驱动重写,这个原则始终成立。
相关推荐

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。