Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Zig Creator Andrew Kelley Talks Zig’s IO Design and Function Coloring Problem (2025-10-10, glm-4.7-flash)

1. 导读

在 Rust 掀起的“借用检查器”热潮和 Go 掀起的“简单至上”共识之间,有一个声音显得格外不合时宜且极具攻击性。Zig 创始人 Andrew Kelley 的这期访谈,不只是在讨论一个新的系统编程语言特性,更是一次对“现代软件工程核心假设”的大胆反叛。他认为,为了获得极致的编译性能、内存安全和开发者的速度感,我们必须打破现有的语言哲学——不再试图用静态分析掩盖底层复杂性,转而极其诚实地将“不安全”和“副作用”暴露在函数签名中。

这种激进的设计哲学之所以在当下显得至关重要,是因为 Richard Feldman 和 Andrew 正在用 Zig 重写一个复杂的编译器。他们面临的不仅仅是语法选择,而是如何构建一个能够容忍运算符重载、无限递归和指针杂乱无章,但依然能在毫秒级间隔内完成增量构建的重型基础设施。这场对话的核心张力在于:我们究竟是在为程序员的安全构建安全,还是为机器的效率构建效率?如果你是一名维护上千万行代码编译基础设施的工程师,你会发现 Andrew 用来解决“缓存”和“链接”难点的方案,可能会颠覆你对“编译执行”的传统认知。

2. 核心观点

Zig 的核心世界观可以概括为**“极致透明与底层法则的胜利”**。Andrew Kelley 认为操作系统和硬件本身就是不安全的,开发者应该直面这种不确定性,而不是试图让语言去包装它。这种观点在业内极具争议,因为它放弃了 Rust 那种“编译器替你思考内存安全”的捷径,转而选择了更原始但更高效的方式:通过显式的上下文参数模拟接口,利用“索引代替指针”实现极致的内存操作,并通过将“副作用”作为可注入参数来解耦逻辑。

以下是该世界观下的四个关键判断:

2.1 IO 抽象的“函数着色”解耦

Andrew 认为,Promise(异步)与 Danger(错误处理)不应绑定在函数签名上,而应绑定在执行上下文中。通过要求所有 IO 操作必须显式接收一个 IO Context 参数(类似 Allocator),语言不再固定函数的“颜色”(是同步阻塞还是异步回调)。

  • 逻辑:语言层只需规定“发生这个动作”,底层实现则决定“它什么时候发生”。这使得从同步切换到异步,或者是扩展出针对测试环境的模拟器接口,成为非破坏性的、局部的工程实践。
  • 背书:Richard 提到,在实际开发中,IO 逻辑往往集中在一个模块,且这种依赖注入的方式使得模拟文件系统(模拟写入失败、模拟随机故障)变得极其简单,为单元测试的稳定性提供了物理基础。

2.2 丢弃编译单元,拥抱内存 Blob

传统的编译流程遵循“源码 -> 对象文件 -> 链接器 -> 可执行文件”。Andrew 断言,对象文件以及中间的硬编码 ABI 是无效的摩擦。编译器应当直接生成执行文件,或者在链接阶段直接写入磁盘。

  • 逻辑:既然编译器已经生成了机器码,却还要将其组合成对象文件交给链接器,这是浪费 CPU 周期。更激进的设想是,将编译过程视为一种内存数据的操作:解析完 IR 后,直接将内存块以 Blob 形式写入磁盘,或者在内存中进行间接引用的“重定位”(Fixup)。
  • 背书:Richard 分享了他们正在构建的编译器:不再依赖 object files,而是直接将 generated code 写入输出。这种方法使得缓存粒度从“函数”变成了“模块”,且读取速度极快(只是一个 pwrite 系统调用)。

2.3 用冗余换取速度:线性扫描哈希表

Andrew 坚信,在哈希表这种高频操作场景下,为了性能可以牺牲 CPU 缓存命中率,使用糟糕的哈希算法来换取简单的线性数组遍历。

  • 逻辑:在现代 CPU 分支预测器的加持下,简单的线性扫描有时比哈希计算更高效。Zig 和 Rock 编译器都在探索“VecMap”策略,即把 key 和 value 存在连续数组中,放弃复杂的内部索引表。
  • 背书:Richard 提到 Folkart Dere 的实验数据表明,只有当集合达到几十万项时,标准哈希表的性能优势才会显现。对于编译器这种局部性极强的场景,线性扫描的常数因子优势碾压了算法复杂度。

2.4 “告知但不阻塞”的编译器哲学

针对 C/C++ 环境中常见的“编译器只是给个提示(Warning),你爱理不理”的顽疾,Andrew 和 Richard 联手构建了一个“寸土不让”的协议:任何警告和语法错误都必须导致非零退出码,但它们不应阻止代码被构建和运行。

  • 逻辑:开发阶段的痛苦在于你只改了一行代码,结果整个编译链因为某个早期检查失败而挂起。Zig 的策略是:只要你还在大脑里思考逻辑,你的代码就可以“脏”着跑(即使是带 Panic 的宽松版本),但这必须失败 CI。
  • 背书:Richard 描述了具体场景:语法错误报警告退出,但生成的二进制文件依然会被构建出来并运行一个 Panic 来告诉你哪里错了。这完美解决了“编译器没报错就误导你做下去”的致命缺陷,同时保留了高频迭代的速度。

内在逻辑链条: 这四个观点共同服务于一个终极目标:打破编译的纸枷锁。通过 IO 参数化解决抽象泄露问题,通过 Blob 化解决链接延迟,通过线性缓存解决内存访问,通过“净输出失败”解决反馈延迟。虽然在传统开发者眼中这些做法充满了“手工地毯式轰炸”的粗暴感,甚至是未经验证的巨大的性能赌博,但对于构建一个涉及数千个编译单元的巨型编译器而言,它们是唯一可行的工程解法。

3. 批判与质疑

若将 Andrew 的方案置于极端压力测试下,其脆弱性随之显现。

首先,“写入时即链接”的战略风险极高。虽然这看似聪明地避开了对象文件格式,但一旦平台发生变化(例如 Apple 的 Mach-O 格式被修改),或者静态链接器需要处理复杂的微架构优化(如内联函数展开),早期的 Hardcoded 逻辑就会变成灾难。这种策略本质上是在用编译器的稳定性去赌底层 ABI 的稳定性,这在快速变化的技术领域是一个危险的赌注。

其次,共享内存与重定位的恐怖谷效应。想象一下,将整个编译器 IR 做成连续的内存块,并通过指针偏移进行序列化。这在纸面上极其优雅——利用虚拟地址空间置换磁盘 I/O。但在实际工程中,只要错了一个偏移量,或者发生一次简单的编译器内存泄漏(Buffer 增长),整个序列化失效,程序直接崩溃,且错误难以定位。这实质上是将“未定义行为”放大到了跨进程边界的级别。

最后,“线性哈希表”的经验主义陷阱。虽然 Folkart Dere 的数据令人惊喜,但在 Zig 的标准库中,ArrayHashMap 默认的阈值可能过小(逻辑中提到可能仅为一个 Cache Line)。Andrew 提出“我们可以调整默认值”,但开发者往往缺乏饥荒年代的敏锐度,容易滥用这种高性能但不可预测的数据结构,导致生产环境中的 Cache 污染和性能倒退。

这场对话并没有试图解决所有的伦理问题,尤其是关于代码的可维护性。随着代码库巨大化,这种高度行话化、充满“魔术数字”和指针操作的解决方案,未来会演变成只有族人才懂的黑洞。

4. 行业视野

Andrew 的访谈并非孤立存在,它准确地卡在了当前系统编程语言演进的一个**“斜坡”**上。

印证了“编译器优先”的文化已经从依赖工具(如 Make, Cargo)转移到了依赖语言设计本身。Rust 通过基于值的所有权和生命周期消除了一个庞大的类别的 Bug(内存安全),而 Zig 则展示了另一条路:通过极其激进的低层控制和极致的编译速度,让开发者自己通过测试和验证来处理安全。这两种范式代表了工具链“去中心化”的两种极端。

同时,这种讨论与 Loris Cro 的 “Asynchrony is not Concurrency” 的理论形成了强势互文。Stephen Cleary 和其他 async-advocates 曾经纠缠于 stackful coroutines,而 Zig 的方案用最原始的手段(回调传递)强行剥离了“并发”的语义,只保留“顺序”的确定性。这种思想正在向 WebAssembly(WASI)的方向迁徙——浏览器需要的就是这种语义(顺序 + OS 抽象),而不是 Rust 那种强并发语义。Andrew 对此的自信表明,一种“非 Rust 式”的、底层控制力极强且编译极快的语言正在成为事实上的补充选项,特别是对于编译器本身、浏览器内核和嵌入式领域。

5. 启示与建议

这场挑战了我们对**“测试覆盖即安全”**的假设。传统观点认为,强大的静态分析和测试覆盖率(如 100% fuzzing)能保证没有内存漏洞。Andrew 的经验表明,在泽字节级(Zettabyte-scale)的代码库中,这种依赖静态成本的工程模式可能会不堪重负。此时,**显式的 error handling(错误处理即控制流)Fuzzer Integration(模糊测试即测试)**比 Borrow Checker(借用检查器)更有效,因为它直接攻击了潜在的逻辑漏洞。

这对以下两类读者最具参考价值:

  • 编译器/语言设计者:不应盲目追求语言的“高级感”(如高级泛型、复杂的异步模型),应优先考虑数据的线性化接口的可解耦性。如果你构建的工具需要处理海量中间数据,一定要将“序列化成本”作为核心工程指标,而不是算法定理。
  • 构建高性能基础设施的架构师:重新审视你现有的“编译 -> 部署”流水线。不要迷信对象文件和标准链接器。思考如何将构建产物视为不可变的内存 Blob,或者是否可以通过依赖注入 PIN(Process-injection)技术来替代传统的重编译流程。

信号与噪音:Andrew 提出的“数组哈希表”是一个强烈的尝试信号,值得验证;而“完全移除链接步骤而只依赖运行时重定位”虽然在理论和 Rock 编译器上行得通,但在通常的应用软件开发中,这依然是噪音,风险远大于收益。

6. 金句摘录

“What Color is Your Function?”

(这句话标志着 Zig 致力于将函数的副作用颜色——无论是同步还是异步——从函数签名本身剥离出来,转而抽象为执行上下文的颜色。)

“Asynchrony means these operations are allowed to happen out of order, but they’re also allowed to happen in order. That’s that’s asynchrony. Interesting. How is that different from concurrency?”

(Andrew 对 Asynchrony(异步) 的定义挑战了业界的直觉。他认为真正的异步是“顺序执行的可能性”,而强制并发才是并发。这为设计不阻塞执行流的 API 奠定了理论基础。)

“If you use our toolset, we will catch dirty pages for you… But this is why we use undefined in Zig. We set those bytes to 0x ABB, which is never mapped in memory, and it always overflows integers.”

(Zig 传统的调试技巧是将未初始化变量设为 undefined,这会在内存中写入特定的模式 0xAB...,用于检测是否混用了未初始化的内存。这种为了性能和调试牺牲清晰度的做法,正是 Zig 精神微服私访的体现。)

“As the title of the article tells you, there is a link to this. I’m going to read about how it works… So now I understand. Yeah. Yeah. Right.”

(这段对话展现了 Andrew 学习新技术的惊人速度。他们在讨论极其复杂的共享内存解释器设计时,思路如同流畅的滑行,这种北美极客文化中的“边学边做”特质是知识转化为工具的关键。)