首页 › programming › 没GC却造出慢GC:Rust管不了图

没GC却造出慢GC:Rust管不了图

导语

Rust 这门语言最响亮的招牌是"没有垃圾回收器(GC),却能保证内存安全"。这话听起来像是天上掉馅饼——既不用忍受 Go 那种运行期 GC 的停顿,又不用担心 C/C++ 那种 use-after-free 的地狱。可是当你真拿 Rust 去写一个图、双链表、DOM 树、编译器 AST 这类天然带环、带反向引用的数据结构时,编译器会逼你把每个节点套进 Rc<RefCell<T>>——引用计数加运行期借用检查。这玩意儿本质上就是一个又慢又差的 GC。

这就是这部 《Zig vs Rust: Who's Closer to the Metal?》 想撕开的那层窗户纸。它把两门"贴近金属"的系统语言摆在一起,核心一句话:Rust 卖的是"保证"(guarantees),Zig 卖的是"控制"(control)。而真正反常识的洞察是——Rust 为了在编译期证明每个指针的寿命,在处理图结构时,被迫手搓出一个比真 GC 还拉的内存管理器;Zig 干脆不证明单个指针,而是把一堆对象塞进一个 arena(内存竞技场),用完一次性释放,反而更省、更快、更好懂。

这篇文章我就带你把这套"借阅检查 vs 显式分配器"的底层账算清楚,并告诉你:什么时候该认怂,别和 Rust 的借阅检查死磕。

视频背景卡片

项目 内容
视频标题 Zig vs Rust: Who's Closer to the Metal?
频道 系统编程科普类频道(视频核心框架:control vs. guarantees)
时长 长视频正片 + 1 分钟配套 Shorts(GTM-YSXF94M)
播放量 新片,增长中(以页面实时数据为准)
核心主题 内存安全该由编译期证明(Rust)还是运行期管理(Zig)
视频金句 "Zig and Rust aim at the same problems from different angles: control vs. guarantees."

说明:因 IP 与配额限制,YouTube 页面元数据未能完整抓取,字幕也未取到。下文视频原话以检索到的描述与配套 Shorts 文案为准,其余为基于公开技术资料与基准测试的延伸分析,已逐段标注。

核心内容拆解

1. "离硬件更近"是个伪命题,真正的分水岭是"不确定性放哪"

【视频观点】视频一上来就拆穿了一个常见误会:Rust 和 Zig 都编译成原生机器码,两者都"贴近金属",比性能参数是打不赢胜负的。真正的分歧在于一个非常哲学的问题——程序里那些"不确定会不会出错"的地方,谁来兜底?

【延伸分析】这里有个干净的二分法:

一句话:Rust 把负担压在编译器和开发者的脑子上,Zig 把负担压在运行期体检和程序员的纪律上。这不是谁对谁错,是两套完全不同的"安全预算"分配方式。

2. 借阅检查的铁律:别名与可变,二选一

【视频观点】视频点出了 Rust 借阅检查最核心的一条铁律——Aliasing XOR Mutation(别名与可变,二选一):同一块数据,要么同时被多个引用读,要么被一个引用写,不能既有别名又可变。

【延伸分析】这条规则对树形结构特别顺滑:父节点独占子节点,所有权层层向下,编译器一眼就能算清谁该被释放。问题出在非树形结构——只要两个节点互相指、或者一个节点被多处引用又需要修改,铁律就卡死了。所以业界有句玩笑:"在 safe Rust 里写一个双链表,难到能出本书。"(这书还真存在,叫 Learn Rust With Entirely Too Many Linked Lists。)这不是 Rust 程序员水平不行,是线性类型系统(linear type system)天然和非线性数据结构八字不合——学术界称之为"阻抗失配"(impedance mismatch)。

3. Rust 的"图结构地狱":双链表被迫套上 Rc

【视频观点】视频用双链表(doubly linked list)当靶子。在 C/Zig 里,一个节点老老实实存两个指针 prev / next 就完事了。在 safe Rust 里,这会形成引用环:A 拥有 B,B 又指回 A,drop checker 算不清谁先释放,借阅检查又禁止插入时同时持有可变别名。

【延伸分析】社区的"标准答案"是把所有东西包进 Rc<RefCell<T>>:

struct Node<T> {
    value: T,
    next: Option<Rc<RefCell<Node<T>>>>,
    prev: Option<Weak<RefCell<Node<T>>>>,
}

这行代码看着人畜无害,代价却藏得很深:

4. 真正的反转:Rc 本质是一个又慢又差的 GC

【延伸分析】这是整部视频(以及我最想强调)的核心反常识。Rc 是什么?引用计数(reference counting),这就是 GC 学教科书里最古老的一种垃圾回收算法。RefCell 的运行期检查,相当于在每次访问时做一次"写屏障"式的校验。

把它们合起来看:Rust 号称没有 GC,却在处理图结构时,被迫手搓出了一个慢速 GC。 而且这个"土制 GC"还两头不靠——它没有真 GC 的并发屏障和分代优化,却背上了手动管理的全部负担(引用计数的原子操作开销、循环引用导致 Rc 还得配 Weak 避免内存泄漏)。

业界有一句狠话:Rc<RefCell> 模型本质上是一个慢速的、临时的垃圾回收器,它既没拿到真 GC 的好处,又吃满了手动管理的所有惩罚。 所以"Rust 没 GC 所以更快"这个直觉,在图密集场景下是站不住脚的。

5. Zig 的破局:arena 把寿命的不变量"上提"到边界

【视频观点】Zig 不走证明单个指针的路子。它的做法是区域式内存管理(region-based memory management):标准库里没有全局 allocator,每个数据结构都要求你显式传一个 Allocator 参数进来。

【延伸分析】Zig 的双链表节点定义,朴素到像在写 C:

const Node = struct {
    value: i32,
    next: ?*Node,
    prev: ?*Node,
};

魔法在管理方式——把一堆节点塞进一个 ArenaAllocator:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var node1 = try allocator.create(Node);
var node2 = try allocator.create(Node);
node1.next = node2;   // 循环引用? 完全没问题
node2.prev = node1;
// 作用域结束时, arena.deinit() 一把全释放

这一招才是精髓。 你根本不需要证明 node1 比 node2 长寿、不需要证明谁先死;你只需要保证一件事——arena 活过这次操作。换言之,Zig 把"寿命证明"这个不变量,从每个对象层面,上提到了一个区域边界层面。证明一个边界,远比证明成千上万个指针容易得多。

性能账也好算:

指标 Rust(Rc) Zig(ArenaAllocator)
每节点内存开销 ~48 字节 ~0 字节
分配策略 逐个 malloc 连续 bump pointer(O(1))
缓存局部性 散落、碎片化 高(空间局部性好)
释放代价 O(N) 引用计数递减 O(1) 整片释放

对编译器、路由引擎、DOM 操作这类图为中心的场景,Zig 的模型不仅更好写,还严格更快——遍历速度能到约 3 倍,因为数据在内存里是连续排布的,CPU 缓存命中率高得离谱。

6. 安全的两种哲学:编译期证明 vs 运行期体检

【视频观点】有人会反驳:Rust 给的是数学证明级的保证,Zig 给的只是"差不多就行"。视频承认这一点——Zig 确实没有 Rust 那种"编译通过就一定没有内存错误"的硬保证。

【延伸分析】但 Zig 不是裸奔。它的 GeneralPurposeAllocator(GPA)在 debug 模式下会主动检测泄漏和 use-after-free:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
    const check = gpa.deinit();
    if (check == .leak) @panic("Memory Leak Detected!");
}

再配合 ReleaseSafe 模式(生产构建也能保留检查,只损失极小性能)、默认检查的整数溢出、切片越界检查,Zig 实务上能在开发阶段抓住大约 99% 的内存错误。没有数学保证,但有工程上够用的反馈环。 很多系统工程师的判断是:为了那最后 1% 的理论保证,付出现实中图结构地狱的代价,不值。这是一个"严格性 vs 工程性价比"的经典权衡。

7. 并发的"着色问题":Rust 的 async 有色,Zig 无色

【视频观点】视频第二个支柱是并发。Rust 的 async/await 是"有色函数"(colored),Zig 的并发是"无色"(colorless)。

【延伸分析】这里的术语来自 Bob Nystrom 2015 年那篇著名文章 《What Color Is Your Function?》。核心吐槽是:一旦你引入 async,函数就被染上了第二种颜色,于是整个调用栈都被污染——同步函数不能直接调用异步函数(得 await),异步函数又会编译成状态机,于是冒出一堆 Pin、Unpin、Send、Box::pin 这种把新手劝退的概念。Rust 里异步递归至今是个痛点,因为编译期必须知道状态机大小,递归 async 只能手动 Box::pin。

Zig 走的是另一条路:函数没有 async 关键字,也没有传统意义上的 await。一次 reader.read() 可能真的阻塞,也可能悄悄挂起一个 green thread——代码长得一模一样。这叫"Green Threads on io_uring"。结果就是一个解析文件的通用函数,能不改一行、不重新编译,同时用在阻塞的命令行工具和高并发的服务器里。"async Rust"和"sync Rust"几乎被诟病成两门语言,而 Zig 只有一门。

8. 选型:保票(Guarantee)vs 控制(Control)

【视频观点】收尾部分视频把结论收得很干净:2026 年的系统编程,本质是在"保票"和"控制"之间做选择。

【延伸分析】这其实是个决策,不是个"谁更强"的站队:

还有个常被忽略的维度:编译速度。Zig 的自托管编译器在 debug 构建时绕过 LLVM 直接吐机器码,改一个函数体,增量编译只要 0.05 秒,而 Rust 要 2.5 秒;干净构建 Zig 约 4 秒,Rust 约 45 秒。对一个反复迭代渲染循环、物理引擎的人来说,这种"瞬时的反馈回路"本身就是生产力乘数。

知识结构图

flowchart TD
    Q["需求: 系统级编程 + 内存安全"]
    Q --> Split{"不确定性放哪兜底?"}
    Split -->|"编译期证明"| R["Rust: 借阅检查 Borrow Checker"]
    Split -->|"运行期管理"| Z["Zig: 显式分配器 Allocator"]

    R --> R1["铁律: 别名 XOR 可变"]
    R1 --> R2["树形结构: 顺滑"]
    R1 --> R3["图/循环结构: 卡死"]
    R3 --> R4["被迫套 Rc RefCell<br/>≈ 又慢又差的 GC<br/>每节点 +48B"]

    Z --> Z1["每个结构带 Allocator 参数"]
    Z1 --> Z2["Arena: 一次 init/deinit"]
    Z2 --> Z3["不必证每指针寿命<br/>只证 arena 活过操作"]
    Z --> Z4["Debug GPA + ReleaseSafe<br/>抓住 ~99% 内存错误"]

    R4 --> C{"选型决策"}
    Z3 --> C
    C -->|"segfault代价极高 / 大团队 / 网络"| PickR["→ Rust: 要保票"]
    C -->|"启动快 / 二进制小 / 嵌入式 / 图为中心"| PickZ["→ Zig: 要控制"]

实战启示

第一,别在图结构上和借阅检查死磕。 如果你写的是编译器 AST、DOM、路由表、游戏场景图、社交关系网这类带环或共享引用的数据,认真评估两个选项:要么上 Zig,要么在 Rust 里用 arena(typed-arena、bumpalo) 模仿区域式内存,而不是无脑 Rc<RefCell>。后者会悄悄把你"没 GC"的项目变成一个带慢速 GC 的项目。

第二,"没 GC"不等于"没内存管理成本"。 Rust 把成本转移到了编译期和你的心智上;Zig 把成本留在了运行期检查和纪律上。选语言时算的是总账,不是单看某个卖点。

第三,给团队画一棵决策树。 segfault 成本极高、团队大、要并发安全 → Rust;启动时间、二进制大小、嵌入式、以图为中心的内核 → Zig;要快速迭代、要"改一行秒级反馈"→ Zig 的编译速度是真优势。

第四,着色问题值得纳入技术评审。 一旦一个项目的调用栈大部分染上 async 颜色,Pin/Send/Box 的复杂度会指数级上升。在 Rust 项目里,能用同步库就别轻易全栈 async;或者考虑把 I/O 密集部分隔离。

第五,工具崇拜要警惕。 2026 年的趋势页(developereducators)上"AI Coding / vibe coding"频道增速最猛,但真正决定系统质量的,依旧是这类内存模型与并发模型的根本选择,而不是用哪个 AI 写代码。

延伸学习路径

  1. Bob Nystrom《What Color Is Your Function?》(2015) — "函数着色"概念的源头,理解 async/await 为何"有毒"的必读。
  2. 《Learn Rust With Entirely Too Many Linked Lists》 — 用四种姿势在 safe Rust 里写双链表,活生生演示借阅检查的痛。
  3. Zig 官方文档的 Allocators 章节 — 读懂 ArenaAllocator、GeneralPurposeAllocator、page_allocator 的分工。
  4. TigerBeetle 设计文档《Designing for Determinism》/ Bun 技术博客 — 看"ZigFat"级别的工业项目如何榨干每个 cycle。
  5. Rust 的 typed-arena / bumpalo crate 文档 — 在 Rust 里复刻区域式内存、缓解图结构地狱的实战方案。

一句话总结

Rust 在编译期逐个证明指针寿命,结果在图结构里被迫手搓出一个慢速 GC;Zig 不证单个指针,只证一个 arena 边界,反而把安全、性能和可读性一次性拿回——所以"没 GC"从来不是终点,"把寿命证明放到哪一层"才是真正的设计战场。

参考来源

  1. Zig vs Rust: Who's Closer to the Metal? — 本文分析的核心视频
  2. Rust vs Zig in 2026: Why Systems Engineers Are Choosing Manual Memory Management — 借阅检查 vs 分配器的深度技术对照(性能基准、代码示例主要来源)
  3. Colored vs uncolored functions — Programming Language Design StackExchange — 函数着色问题的学术讨论
  4. Why Zig Is Winning Hearts Where Rust's Borrow Checker Struggles — Medium — 社区视角的补充观点
  5. Rust vs. Zig: The New Programming Language Battle for Performance — dev.to — 嵌入式与裸金属场景的对比

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入