乐于分享
好东西不私藏

38. 高性能软件为什么越来越关注 Zig

38. 高性能软件为什么越来越关注 Zig

一、问题

先不谈 Zig。

先问一个更朴素的问题:为什么做高性能软件的人,最近几年普遍变得焦虑?

这个焦虑不是空穴来风。游戏引擎团队在 60fps 甚至 120fps 的帧预算里,留给"逻辑代码"的时间窗口从来没有这么紧张过——一帧 16.6 毫秒,扣掉渲染、物理、音频,留给 Gameplay 逻辑的往往只有两三毫秒,任何一次意外的堆分配、任何一次缓存未命中导致的停顿,都可能是掉帧的直接原因。数据库存储引擎团队在追求的是另一种极限:P99.9 延迟不能有毛刺,而毛刺的头号元凶,长期以来就是不可预测的内存分配路径和垃圾回收暂停。高频交易系统更极端,纳秒级的抖动都要被当作事故去排查。

这些团队几十年来一直用 C 和 C++ 解决问题,而且解决得还不错。那为什么最近几年,越来越多做高性能软件的团队开始认真评估一门连 1.0 都还没发布的年轻语言?

答案不是"Zig 更快"——这个说法本身就站不住脚,因为 Zig 编译到 LLVM IR,理论性能天花板和精心优化的 C 没有本质差异,真正的问题从来不是"哪门语言跑得快",而是**"哪门语言让写出稳定的高性能代码这件事,不再依赖工程师的超人记忆力"**。

C 能写出极致性能的代码,前提是每一个团队成员都严格自律:永远记得检查这次调用会不会触发隐藏分配,永远记得这个第三方库有没有偷偷调用 malloc,永远记得手写的内存池边界条件是不是处理对了。这种自律在十人团队、五万行代码规模下还能维持,在五十人团队、五十万行代码规模下,几乎必然会在某个疲惫的周五下午失守。高性能软件团队焦虑的核心,从来不是"能不能写出快代码",而是"能不能让快代码在团队规模扩张之后依然保持快"。


二、历史

高性能软件对底层语言的选择史,其实是一部不断在"控制力"和"人力可持续性"之间寻找平衡点的历史。

汇编时代给了程序员最彻底的控制力,但代价是完全不可移植、可维护性极差,超出个人能记住的规模就会失控。

C 语言的出现是第一次重大平衡:保留接近汇编的控制力(手动内存、可预测的指令序列),换取了可移植性和基本的抽象能力。这个平衡持续统治高性能领域四十多年,id Software 的 Doom、Quake 系列,Linux 内核,几乎整个游戏行业和操作系统行业的性能敏感部分,都建立在这个平衡点上。

C++ 的介入试图在不牺牲性能的前提下加入更高层的抽象——面向对象、模板、STL 容器。这在很长时间里确实提高了大型高性能项目的可维护性,虚幻引擎、大多数 3A 游戏引擎、绝大多数商业数据库内核,今天依然是 C++ 的天下。但 C++ 的抽象是有隐藏代价的:虚函数表带来的间接跳转会打断 CPU 分支预测和指令流水线;异常处理机制即便不触发也会在二进制里留下额外的表结构和体积开销;STL 容器的默认分配器策略往往不是性能敏感场景想要的那一种,很多高性能团队最终选择自己重写一整套容器和内存分配逻辑,绕开标准库。

Rust 的出现给这个领域带来了一次严肃的挑战者——它证明了"零成本抽象 + 编译期内存安全"可以同时做到,不需要垃圾回收器。这直接催生了一批 Rust 写的高性能基础设施项目(如 TiKV、Materialize),业界普遍认可其性能可以做到和 C++ 同一量级。但一个现实问题始终存在:Rust 的所有权和借用检查器,在对着"数据结构本质上就充满共享和环状引用"的高性能场景(比如游戏里 ECS 架构中互相引用的实体、图数据库里天然的图结构)时,经常需要工程师绕过借用检查器的直觉,用 unsafe、索引代替引用等方式"曲线救国",这在一定程度上抵消了 Rust 本来想避免的心智负担。

高性能软件行业走到今天,缺的其实是这样一门语言:保留 C 那种"所见即所得、没有隐藏开销"的直觉,同时提供比 C 更好的工具去管理内存生命周期,而不强迫工程师接受一整套新的抽象心智模型


三、失败案例

这个赛道里有不少真实的、代价高昂的教训。

C++ 异常在高频交易系统里的真实事故:多个金融机构的技术团队在公开的技术分享中都提到过同一类问题——异常处理机制在正常路径不触发时看似零开销,但异常表本身占用的二进制体积会影响指令缓存的命中率,而一旦某个边界条件意外触发异常,栈展开的耗时是不可预测的、可能达到微秒甚至更高量级,这对于要求纳秒级确定性的交易系统是灾难性的。这也是为什么几乎所有严肃的高频交易 C++ 代码库都会强制关闭异常(-fno-exceptions),退化成用错误码传递错误——本质上是绕过了 C++ 语言层面提供的错误处理机制,自己发明一套更接近 C 的做法。

STL 容器默认分配策略引发的游戏引擎性能事故是另一类经典案例。多个公开的游戏后处理复盘(GDC 分享中反复出现的模式)指向同一个问题:std::vector 在扩容时的默认策略、std::string 的小字符串优化边界、std::map 基于红黑树的节点级分配,都会在高频调用路径上产生大量细粒度的堆分配请求,这些分配请求本身在多线程场景下还会竞争全局堆分配器的锁,进一步放大延迟毛刺。几乎所有严肃的游戏引擎团队最终都自己重写了一整套容器库,绕开标准库的默认行为——这意味着团队要额外维护一整套"影子标准库",长期来看是巨大的隐性成本。

Rust 在实时音频处理领域的水土不服同样是真实存在的问题。实时音频处理有一条铁律:音频回调线程里绝对不能发生堆分配、绝对不能加锁,因为任何一次意外阻塞都会在扬声器里表现成听得见的爆音。Rust 的所有权模型和这类"预先规划好的静态内存池 + 无锁数据结构"场景配合得并不天然顺畅——很多 Rust 音频库的作者在公开博客中坦承,为了在音频回调路径彻底避免堆分配,不得不写大量 unsafe 代码绕开借用检查器,这在一定程度上偏离了 Rust 最初想要提供的安全保障。

这三类案例指向同一个结论:高性能软件真正需要的,不是"更高级的抽象",也不完全是"编译期证明的安全性",而是一种能让"这段代码到底有没有碰堆内存、有没有可能阻塞"这件事,变得肉眼可判断的语言机制


四、Zig 的设计思想

Zig 对这个具体痛点的回应,在设计哲学上比前面几篇文章讨论的更宏观的"内存安全"命题要窄得多、也更精准——它几乎是专门针对"隐藏开销"这一件事设计的。

Andrew Kelley 在多次面向游戏和实时系统开发者的演讲中,反复强调一个观点:性能优化真正的敌人不是"某个具体操作慢",而是"你不知道某个操作会不会执行"。C++ 的操作符重载可能悄悄触发一次深拷贝;智能指针的引用计数递增递减看起来是一行代码,实际是一次原子操作;异常处理路径可能在你完全没预料到的地方展开栈。这些"意外"才是高性能代码里最难排查、也最反直觉的性能陷阱来源。

Zig 的应对策略是把"没有意外"提升为语言的第一原则,具体体现为几条几乎不留余地的规则:没有操作符重载,意味着 a + b 永远只能是一次简单的算术运算,不可能是一次隐藏的函数调用;没有隐式类型转换,意味着不会有编译器悄悄替你插入一次可能有性能代价的转换;没有构造/析构函数的隐式调用,意味着一个结构体变量离开作用域时,不会有任何代码在背后偷偷执行——如果你想要资源释放,必须显式调用 defer 语句,这行代码就明明白白写在那里,不需要去翻类定义才能知道会发生什么。

这套设计哲学换来的直接收益是:审阅一段 Zig 代码的性能特征,不需要理解整个类型系统和继承链,只需要读这几行代码本身。这对于需要频繁做 code review、需要新人快速上手性能敏感代码库的高性能团队而言,是实打实的工程效率提升,而不只是审美偏好。


五、实现机制

具体到实现层面,Zig 有几个机制是专门为高性能场景服务的。

编译期已知大小的类型系统是第一个关键。Zig 的结构体、数组在绝大多数情况下大小在编译期就完全确定,配合 comptime 可以在编译期完成边界检查、循环展开、甚至整个查找表的生成,这些计算完全不落入运行时开销:

运行时方案:
运行时查表 → 每次访问一次内存读取 + 可能的缓存未命中

comptime 方案:
编译期生成常量表 → 直接内联进指令流
                  → 甚至可能被编译器进一步优化成立即数

这也是 Zig 社区里高性能数值计算、查找表生成类代码经常用 comptime 而不是运行时初始化的原因。

SIMD 向量类型是语言内建的一等公民@Vector(N, T) 这样的语法直接暴露 SIMD 寄存器宽度的向量运算,不需要像 C 那样依赖编译器内建函数(intrinsics)或者祈祷自动向量化生效。这对图形处理、物理模拟、音频 DSP 这类天然适合 SIMD 的高性能场景是直接的生产力提升。

编译目标的精细控制上,Zig 允许精确指定目标 CPU 的微架构特性(比如是否启用 AVX2),并且交叉编译到不同 CPU 目标的体验和编译到本机一样简单,这对需要针对不同硬件平台分别调优的游戏和高性能计算团队非常关键——过去这类工作往往需要一整套复杂的交叉编译工具链配置。

运行时安全检查的可分级控制是另一处对性能极度友好的设计。Zig 提供四种构建模式:Debug(全量安全检查,包括数组越界、整数溢出)、ReleaseSafe(保留安全检查但优化代码)、ReleaseFast(关闭安全检查追求极致性能)、ReleaseSmall(优化二进制体积)。这让团队可以在开发阶段享受完整的运行时保护,排查问题效率更高,同时在最终发布的高性能场景下,一个编译参数就能拿掉这些检查的开销——不需要像 C 那样,安全检查这件事从一开始就得自己手写、自己决定要不要加。


六、源码层面的思想

Zig 标准库里为高性能场景准备的几个关键模块,体现出的设计思路值得单独拆解。

std.ArrayList 在扩容策略上采用简单的倍增算法,但关键在于扩容行为完全对调用方可见和可控——可以预先调用 ensureTotalCapacity 避免运行时反复扩容,这在性能敏感的热路径代码里是常规操作,而这个 API 之所以存在,正是因为 Zig 团队认为"容器什么时候扩容"不应该是一个对使用者隐藏的黑箱决策。

std.heap 命名空间下并存着多种分配器实现,其中 FixedBufferAllocator 直接从一块预先分配好的静态内存或栈上内存里切分空间,完全不涉及系统调用,这是实时音频、嵌入式场景里彻底规避堆分配的标准做法——和前文提到的 C++/Rust 音频开发者不得不用 unsafe 绕开语言限制不同,Zig 里这是一条被官方标准库直接支持、不需要绕开任何机制的正常路径。

@atomicRmw@fence 这类原子操作和内存屏障相关的语言内建函数,直接暴露底层 CPU 提供的原子指令语义,不经过任何额外的抽象层封装,这对编写无锁数据结构的团队来说,意味着生成的机器码和手写汇编几乎没有差距,同时依然保有 Zig 语言层面的类型检查。

编译器自身在代码生成路径上,近年持续投入的自研 x86-64 后端(绕开 LLVM 生成机器码),核心动机之一正是缩短高性能项目的迭代编译周期——对于需要频繁做性能调优、反复编译测试的团队,编译等待时间本身就是生产力的一部分,这也是为什么这个自研后端的进展会被高性能软件社区密切关注。


七、性能分析

分支预测友好性方面,Zig 没有虚函数表带来的间接跳转(除非显式构造函数指针接口),函数调用默认是直接调用,CPU 分支预测器和指令预取器对这类调用模式的预测准确率远高于虚函数间接跳转,这一点和精心优化过、关闭了运行时多态的 C 代码处于同一水平。

内存分配路径的确定性是 Zig 相较传统 C++ 高性能代码的一处结构性优势——不是因为 Zig 的分配器本身运行得更快,而是因为分配路径永远显式可见,性能审计时不需要逐层展开类定义和继承链去确认某个方法会不会碰堆内存,直接看函数签名有没有 Allocator 参数就能得出结论。这在代码审查环节节省的时间,对大型团队而言是实打实的效率提升。

C++ 场景审计一次内存分配路径:
函数调用 → 查看类定义 → 查看基类 → 查看是否有隐藏的
operator new 重载 → 查看容器成员的默认分配器 → 确认

Zig 场景审计一次内存分配路径:
函数调用 → 查看签名是否有 allocator 参数 → 确认

二进制体积和启动时间上,Zig 没有 C++ 运行时那一整套异常处理表、RTTI 元数据、静态初始化顺序机制,编译产物往往比同等功能的 C++ 程序更小、启动更快,这对需要频繁冷启动的 CLI 工具、Serverless 场景、嵌入式固件是直接收益,也是 Bun 这类工具选择 Zig 时反复强调的一项指标。

跨平台一致性方面,由于交叉编译和目标 CPU 特性控制是语言工具链原生支持的能力,团队为不同硬件平台(比如同时支持 x86-64 服务器和 ARM64 边缘设备)分别做性能调优时,不需要维护一整套额外的交叉编译基础设施,这个隐性成本的降低在高性能团队的实际工程投入统计里往往被低估。


八、对比分析

聚焦"高性能软件适用性"这一个维度,把几种主流选择放在一起客观对比:

维度
C
C++(关闭异常/RTTI 的高性能子集)
Rust
Zig
隐藏开销
需要团队自律地关闭特性才能做到少
较少,但 Drop/引用计数等仍有隐藏成本
语言层面刻意消除,默认状态就是"少"
手写容器库的必要性
高(标准库能力有限)
高(默认 STL 行为不适合性能场景)
中(标准库设计已考虑性能,但所有权模型偶尔需要绕开)
中低(标准库设计从一开始就为显式控制服务)
SIMD/底层控制
需要 intrinsics,体验较原始
需要 intrinsics 或第三方库
需要 unsafe 或第三方 crate
语言内建 @Vector,体验更直接
编译期计算能力
弱(宏是文本替换)
中(模板元编程/constexpr,语法复杂)
中(const fn,能力持续扩展中)
强(comptime 与运行时同一语法)
团队规模化维护成本
高(依赖个人自律)
高(需要自建大量替代 STL 的基础设施)
中(借用检查器分担部分负担,但学习曲线本身是成本)
待验证(生态和长期案例仍不充分)
生产落地成熟度
极高
极高
高,持续增长
有分量但数量有限的案例(TigerBeetle、Bun 等)

这张表格里最需要强调的诚实结论是:**Zig 目前的优势更多是"设计上更适合",而不是"已经被大规模验证更适合"**。C++ 高性能子集经过几十年生产验证,Rust 在这个领域也已经有 TiKV 这类经过大规模生产考验的案例,Zig 的案例集虽然含金量高,但数量和时间跨度都还处于早期阶段——这个判断需要留在结论里,而不能被"设计精巧"这件事掩盖。


九、工程价值

对高性能团队而言,评估要不要引入 Zig,值得拆成几类具体场景分别判断。

新启动的、性能是核心竞争力的基础设施项目(数据库存储引擎、消息队列、实时通信服务端),是目前 Zig 生产落地案例最集中、也最有说服力的场景——TigerBeetle 团队公开的技术博客反复提到,选择 Zig 的核心原因正是"确定性内存分配 + 没有隐藏控制流"直接对应他们对延迟尾部的极致要求,这类团队通常本身就有很强的系统编程能力,能够承担语言尚未 1.0 带来的风险。

游戏引擎的性能敏感子系统(物理引擎、渲染管线的底层部分)是另一个正在被认真评估的场景,尤其是新启动的独立游戏引擎项目,Zig 提供的 comptime 元编程能力和 C 级别的控制力,对需要针对不同平台高度定制内存布局的场景很有吸引力,但大型商业引擎(虚幻、Unity 这个量级)由于既有 C++ 代码库体量巨大,短期内迁移到 Zig 的可能性很低,更现实的路径是局部工具链、局部模块采用。

已有大型 C/C++ 代码库的渐进式性能优化,是 Zig 目前投入产出比最高的一类应用场景——不需要重写整个系统,只需要把某几个被反复证明是性能瓶颈、且团队愿意承担一定风险的模块用 Zig 重写,借助原生的 C 互操作能力直接接入既有代码库。多家公司在技术分享中都提到,用这种"外科手术式"的局部替换,而不是整体重写,是性价比更高的落地路径。

不建议的场景同样需要明确:如果团队的性能问题主要来自算法层面而不是语言层面的隐藏开销,换语言不会解决根本问题;如果项目对长期 API 稳定性有硬性要求(比如需要五年以上不做破坏性升级),Zig 目前的 1.0 前状态是一个无法回避的风险敞口,这一点在前一篇讨论"系统编程未来"的文章里已经详细展开,这里不再重复。


十、行业影响

如果 Zig 在高性能软件这个细分领域持续扩大生产落地案例,最直接的行业影响会体现在**"手写替代标准库"这件事的必要性会显著降低**。前文提到的、几乎所有严肃 C++ 高性能团队都要经历的"重写一套容器库绕开 STL 默认行为"这个隐性成本极高的过程,如果 Zig 标准库从设计源头就避免了这个问题,会实质性降低新项目启动高性能系统的门槛——这个影响对中小规模团队的意义,可能比对已经有成熟内部基础设施的大厂更大。

另一个值得关注的影响方向是,Zig 在编译期计算和显式内存控制上展示出的可行性,正在反过来影响其他语言社区对"高性能友好特性"的讨论——C++ 的 std::allocator 相关提案、Rust 社区关于改进分配器 API(allocator_api 相关的长期讨论)都能看到对 Zig 显式 Allocator 模式的直接参照。这种"设计思想外溢",即使不依赖 Zig 语言本身的最终市场份额,也已经在实质性发生。

但如果 Zig 在这个领域没能持续扩大影响力,最可能的瓶颈会是生态工具链的完善速度跟不上团队的实际需求——高性能软件团队往往依赖大量专业化的性能分析工具(火焰图生成、缓存命中率分析、指令级 profiling),这些工具在 C/C++/Rust 生态里已经高度成熟,Zig 生态目前这块的完善度仍有明显差距,这是比语言设计本身更容易被低估、但实际决定采用与否的关键因素。


十一、未来思考

高性能软件团队对 Zig 的关注,本质上不是被某个 benchmark 数字打动的,而是被一种更朴素的诉求打动的:他们想要一门语言,能让"这段代码到底有没有隐藏开销"这件事,重新变成一眼就能看穿的事情,而不是需要靠团队集体记忆和层层代码审查才能守住的纪律

C 给过这种确定性,代价是把内存安全的全部责任留给程序员;C++ 试图在保留确定性的同时加入抽象能力,代价是抽象本身悄悄带回了不确定性;Rust 用编译期证明找回了安全,代价是在某些天然适合共享和环状结构的高性能场景里,工程师需要绕开语言本身的直觉去达成目的。

Zig 提出的答案,某种意义上是一种"做减法"的答案——不去发明新的抽象机制去解决隐藏开销的问题,而是从语言层面直接砍掉那些容易滋生隐藏开销的特性本身。这条路径能不能在五年、十年的时间尺度上,经得起更大规模、更长周期的生产项目考验,目前还没有足够的样本量给出确定的结论。

但可以确定的是,高性能软件行业对"确定性"这件事的渴望,从来没有像今天这样迫切过——当每一毫秒、每一次缓存未命中都开始被认真计较时,一门愿意为了这种确定性牺牲部分表达力的语言,至少值得被认真放上评估的桌面。

至于它最终会不会成为高性能软件的默认选择,答案不会写在这篇文章里,而会写在未来几年 TigerBeetle 们能不能扛住十倍、百倍的生产流量,以及会不会有下一个同量级的项目愿意跟进这个选择。