第8.10章: 为什么“零开销”与“零成本”毫无意义
概述
“零开销”和“零成本抽象”这两个说法在 C++ 与 Rust 社区里被反复念诵。它们出现在会议演讲、Reddit 帖子、博客文章,甚至标准委员会的讨论里。但这些口号不是工程概念,而是营销话术——更糟的是,它们常常被用来封杀那些有细致分析和证据的讨论。
本章将考察这些口号为什么在实践中毫无意义,为什么它们被习惯性地误用,以及为什么现实世界中的性能取决于二进制体积、存储层次结构、平台约束和经济成本,而不是口号。
1. 作为宣传的口号
在很多在线社区(r/cpp、r/rust 等)里,“零开销”被当作修辞武器使用。人们不去讨论取舍,而是直接断言:
“It’s zero‑overhead, so your argument is invalid.”
这不是工程,这是宣传。它阻止人们讨论:
- 二进制体积
- 指令缓存压力
- 重定位代价
- 优化器复杂度
- 可调试性
- 可移植性
- 经济成本
这些没有一项是“零”。这些没有一项可以被优化掉。
2. 异常处理不是零开销
C++ 异常处理常被宣传为“零开销”,理由是正常路径中没有显式检查。但这忽略了一些真实代价:
- 更大的二进制体积
- 展开表与元数据
- 更多的重定位项
- 更少的优化机会
- 更差的指令缓存局部性
对于崩溃是可接受的应用(也就是大多数现代应用)来说,std::abort() 显然更便宜。
Linux 的 overcommit 会杀掉进程,Android 和 iOS 会未经许可杀掉应用。用户并不在意,他们照样买手机。
所以说异常处理是“零开销”的说法根本不成立。
参见 Raymond Chen 的文章: “Zero-cost exceptions aren’t actually zero cost” 。
3. lambda 不是零开销
lambda 会复制代码。即使两个 lambda 的函数体完全相同,它们也会各自生成独立的实例。这增加了:
- 二进制体积
- 指令缓存压力
- 重定位代价
更大的二进制加载更慢、映射更慢,优化器也更难对它进行推理。这些没有一项是“零开销”。
4. 在 Windows x64 上 span 与 string_view 不是零开销
一个常见的说法是 std::span 和 std::string_view 是“零开销”抽象。这在 Windows x86‑64 上并不成立,原因非常具体:Windows x64 调用约定只把大小为 1、2、4、8 字节的参数放在寄存器里传递。
在 64 位平台上,std::span 和 std::string_view 都是 16 字节的聚合体(一个指针加一个大小)。在 Windows x64 上,这类 16 字节类型不满足寄存器传递的条件,因此总是通过内存传递。这纯粹是 ABI 规则,不是 STL 或 MSVC 的 bug。
在 32 位(i686)与 Windows on ARM 上,以及在 SysV x86‑64(Linux、BSD、macOS)上,16 字节聚合体可以通过寄存器传递,因此同一份 span/string_view 代码在不同平台上的调用约定代价并不相同。Godbolt 上的例子清楚地展示了 64 位 Windows 与 32 位以及 SysV ABI 之间的这种差异。
关键在于:“零开销”并不是某个抽象的普遍属性,它取决于 ABI。一个在 SysV 上看起来“零开销”的抽象,在 Windows x64 上可能贵得多。平台规则压倒口号。
5. 指令顺序与优化器复杂度
甚至只是改变指令的顺序也会影响性能。优化器必须推理依赖关系、别名关系和调度。
更多抽象 → 更多中间表示 → 更高复杂度 → 更少的优化。这就是开销,它不是零。
6. C++ 被采用是为了可移植性,而不是性能
很多人声称 C++ 被采用是因为它“快”。实际上,C++ 被采用是因为它:
- 可移植
- 稳定
- 到处都有支持
- 与现有代码兼容
性能往往是次要的,在许多场合甚至无关紧要。
关于这一点,我们会在讲 freestanding 与渐进式 Web 应用的那一章里进一步讨论,也会讨论移动应用的沙箱机制(Android、iOS、UWP)如何让原生应用启动更慢、更新更慢、分发更难。
7. PWA 可以比原生应用更快
考虑一个简单的媒体播放器。一个 PWA 可以:
- 更小(不到 100 KB)
- 启动更快(浏览器已经在运行)
- 默认跨平台
- 更安全(浏览器沙箱)
- 更新成本更低
而 VLC 的安装包超过 100 MB,SMPlayer 也差不多。更新代价高,启动更慢,因为整个应用都必须从磁盘加载。
我的 PWA Player 就是一个具体例子。播放器本身不到 100 KB,而浏览器中那个用高度优化的 C/C++/汇编写成的媒体引擎早已加载在内存中,并在多个进程之间共享。PWA 并不重新实现媒体引擎,它只是使用浏览器已经提供的那个。
这种架构让一个轻量的 PWA 在启动时间、更新成本和分发便利性上胜过重量级的原生应用。它直接反驳了“C++ 总是更快”这种简单化叙事。
8. 经济成本是实实在在的开销
增加异常处理这类特性会消耗:
- 委员会的时间
- 编译器工程资源
- 维护成本
- 测试成本
这些资源本可以用于:
- 更好的优化
- 更好的工具
- 更好的诊断信息
- 更好的可移植性
经济成本就是开销。它虽然间接,却显著地影响着现实世界中的性能。
参见 Chandler Carruth 在 CppCon 2019 的演讲:
要点
- “零开销”和“零成本”是口号,不是工程术语。
- 它们常常被用来终结讨论,而不是促进讨论。
- 异常处理、lambda、span、string_view 都有真实代价。
- 二进制体积、指令缓存压力与重定位代价都很重要。
- 平台差异很重要;没有任何东西是普遍零开销的。
- C++ 被采用主要是为了可移植性,而不是性能。
- PWA 之所以能胜过原生应用,是因为浏览器已经在运行。
- 经济成本是开销;委员会的时间不是免费的。
- 不存在零成本抽象——只有取舍。