首页 / 文章 / Zig 的新 bitCast 语义与 LLVM 后端改进
← 返回
AI技术

Zig 的新 bitCast 语义与 LLVM 后端改进

✍️ zhirenhun 📅 2026/6/26 👁 94 阅读 ⏱ 32 分钟
Zig 的新 bitCast 语义与 LLVM 后端改进

Zig 的新 bitCast 语义与 LLVM 后端改进

2026-06-25
作者:Matthew Lugg
(这篇开发日志有点长,抱歉——我写得太投入了!)

几周前,我开始在一个分支上工作,旨在实现对 LLVM 后端的一项改进,这项改进已经计划了很长时间。最终,这项工作像滚雪球一样演变成了一项更大的变更,实现了一些你可能会感兴趣的语言提案。

LLVM 后端整数降级

Zig 一直将任意位宽的整数类型(例如 u4i13u40)直接降级为 LLVM IR 的 bit-int 类型(i4i13i40)。然而,我们很久以前就知道这种降级方式并非最优,因为 LLVM 关于这些类型在内存中表示的文档化语义对优化器施加了不必要的限制。或许更重要的是,由于 Clang 从未以这种方式生成 LLVM IR,LLVM 中的这些代码路径从未得到过适当的测试,因此在实践中支持得很差——在过去几年中,我们观察到许多遗漏简单优化甚至直接错误编译的实例。

因此,该 PR 的原始目标是仅在以 SSA 形式操作值时使用这些 bit-int 类型,并在将其存储到内存时将其零扩展或符号扩展为 ABI 大小的类型(i8i16i32 等)。这应该能得到很好的支持,尤其是因为它与 Clang 降级 C 的 _BitInt(N) 的方式相匹配!

这个改动实际上相当直接,但我遇到了一个问题,这让我陷入了一个小小的困境。

@bitCast 的问题

@bitCast 是一个有趣的内置函数。过去,它被定义为等同于以下操作序列:

  • 获取指向操作数值的指针
  • 将其转换为指向目标类型的指针
  • 从该指针加载

换句话说,它本质上是重新解释内存字节的语法糖。然而,随着时间的推移,我们偏离了这个定义——例如,它被允许使用 @bitCast[3]u8 重新解释为 u24,尽管在大多数目标上 @sizeOf(u24) 大于 @sizeOf([3]u8),因此上述定义会引发非法行为。

到目前为止,LLVM 后端一直为 @bitCast 内置函数实现这些未明确规定的语义。然而,由于该定义涉及重新解释内存,改变我们在内存中存储整数类型的方式最终影响了 @bitCast 的实现,并引入了导致编译器测试套件崩溃的非法行为。

解决这个问题最简单的方法可能是在 LLVM 后端实现逻辑以大致匹配旧的行为。我反而选择了一个更好的解决方案——为 @bitCast 实现一个新的定义。

重新定义 @bitCast

2024 年,Jacob Young 撰写了语言提案 #19755,旨在通过精确指定一组新的语义来解决 @bitCast 的问题。该提案在提交后不久就被接受了,事实上,它详述的语义已经由自托管的 x86_64 后端实现!因此,为了解决 LLVM 后端的问题,我不一定需要匹配旧的 @bitCast 语义——相反,这似乎是最终在所有地方实现新语义的好时机。

顺便提一下,这样做的另一个好处是我们可以利用编译器的 Legalize 过程,该过程获取难以降级的操作并将其重写为更简单的操作,这样编译器后端只需要支持这些简单操作。Legalize 已经具有由自托管 x86_64 后端使用的功能,可以将复杂的 @bitCast 操作转换为更简单的操作,并且可以很容易地适应以帮助其他编译器后端(主要是 LLVM 和 C 后端)——但前提是它们实现了新语义。

无论如何,关键是,我开始了一项支线任务(最终比原始任务更难),在整个编译器中实现这些新语义。这不仅包括 LLVM 和 C 后端,还包括 comptime 执行——毕竟,Zig 允许你在编译时执行几乎任何操作,包括 @bitCast!由于新语义与旧语义有显著不同(稍后会详细介绍),我还必须审计标准库、编译器和支持库(例如 compiler_rt)中大量 @bitCast 的使用。但在对 CI 失败进行了一些基本无痛的修复后,我终于让我的 PR 通过了所有检查,并在昨天合并到了 master 分支(在此过程中关闭了不少问题!)。

新的 @bitCast 语义

既然我们已经了解了所有背景,现在是时候真正解释新的 @bitCast 行为了。与以前基于重新解释内存中的字节不同,该内置函数现在根据逻辑上表示类型的位来定义。

每个支持 @bitCast 的类型都有一个“逻辑位布局”——该类型作为有序位序列的表示。例如,u5 由 5 个逻辑位组成,我们按从最低有效位到最高有效位的顺序排列。[2]u5 由 10 个逻辑位组成——第一个元素的 5 位,后跟第二个元素的 5 位。@bitCast 的新定义是它将一种类型的逻辑位重新解释为另一种类型的逻辑位。

最简单的例子是取一个无符号整数,比如 u8,并将其转换为相同大小的有符号整数,即 i8。此操作完全符合你的预期——位不变,我们只是将最高有效位重新解释为符号位。整数类型与 packed struct / packed union 类型之间的 @bitCast 语义也保持不变。

新语义与旧语义不同的地方在于涉及聚合类型(数组和向量)时。

例如,考虑将 [2]u8 位转换为 u16。在旧语义下,此操作的结果取决于目标端序:在大端序目标上,第一个数组元素成为 8 个最高有效位,而在小端序目标上,第一个数组元素成为 8 个最低有效位。在新语义下,因为我们只关心逻辑位表示(与端序无关),该操作在每个目标上的行为都相同:第一个数组元素成为 8 个最低有效位。作为一般规则,新语义倾向于匹配小端序目标上旧语义的行为。

这个定义还允许一些更奇怪的操作,例如将 [2]u3 转换为 @Vector(3, u2)

test "bitcast [2]u3 to @Vector(3, u2)" {
    const arr: [2]u3 = .{ 0b001, 0b011 };
    const vec: @Vector(3, u2) = @bitCast(arr);
    // 连接 `arr` 的所有位,从 `arr[0]` 的最低有效位开始,以找到
    // 逻辑位序列,然后从中读取 2 位块以获取结果向量值 `vec` 的元素。
    //
    // arr[0]   arr[1]
    // 0b001    0b011
    // ------------- -------------
    // 1 0 0    1 1 0
    // -------- -------- --------
    // 0b01     0b10     0b01
    // vec[0]   vec[1]   vec[2]
    try expect(vec[0] == 0b01);
    try expect(vec[1] == 0b10);
    try expect(vec[2] == 0b01);
}
const expect = @import("std").testing.expect;

这种操作在大多数时候不是很有用,但如果你需要,它就在那里!例如,也许你想将一个整数解构为单个位的向量来进行操作——现在可以通过 @bitCast@Vector(n, u1) 来实现。

在做所有这些事情的同时,我还实现了一些较小的已接受提案——我不会在这里详述,但如果你感兴趣,可以看看这些问题:

当然,所有这些改变的语义都将在 0.17.0 版本发布说明中解释(希望比我在这里写的更简洁!),并概述建议的迁移步骤。

LLVM 后端性能

最后,我只想提一下,这个分支的原始动机——改变 LLVM 后端降级非 ABI 整数类型的方式——在恢复遗漏的优化方面是明显成功的。事实上,Zig 编译器本身——尽管内部没有大量使用任意位宽整数!——通过更好的优化看到了大约 5% 的性能提升。这意味着你可能期待在 0.17.0 中获得一些微小的运行时性能提升!

感谢阅读,我希望这对你们中的一些人来说很有趣。祝编码愉快!


ELF 链接器改进

作者:Matthew Lugg

过去几周,我一直在研究我们在 Zig 0.16.0 中首次亮相的新 ELF 链接器。在 0.16.0 发布时,这个链接器实现还处于相当早期的阶段,并且只真正支持链接没有外部库(甚至没有 libc)的纯 Zig 代码——这就是为什么它(现在仍然)默认禁用(可以通过 -fnew-linker 启用)。然而,自最初发布以来,已经取得了相当大的进展!

这是一个不错的里程碑——根据我最近的 PR,新的 ELF 链接器能够构建启用了 LLVM 和 LLD 库的自托管 Zig 编译器,这项任务需要相当多的底层功能。

[mlugg@nebula master]$ # 使用新链接器构建 Zig 编译器:
[mlugg@nebula master]$ zig build -Dno-lib -Dnew-linker -Denable-llvm
[mlugg@nebula master]$ # 使用该编译器构建带有 LLVM 和 LLD 的东西:
[mlugg@nebula master]$ ./zig-out/bin/zig build-exe ~/hello.zig -fllvm -flld
[mlugg@nebula master]$ ./hello
Hello, World!
[mlugg@nebula master]$

当然,ELF 链接器不一定是世界上最令人兴奋的东西,这就是为什么这个新链接器的头条功能是支持快速增量编译。在最近的增强之后,现在(在 x86_64 Linux 上)可以在链接外部库、C 源文件等时执行增量重建——没有任何额外的性能开销!这是我在 Andrew 的俄罗斯方块克隆版上尝试它的片段:

哦,快速增量重建在 Zig 编译器本身上也运行良好:

[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 36s
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 244ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 228ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 288ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 283ms

这个链接器实现目前最大的缺失功能是它仍然不支持为 Zig 代码生成 DWARF 调试信息——这绝对是我的下一个优先事项。但即使没有这种支持,即时重建的实用性也是惊人的,例如在你进行大量打印调试的任何情况下。

如果你正在使用 Zig 的 master 分支并且你在 x86_64 Linux 上,考虑尝试使用新的 ELF 链接器进行增量编译,如果它以前在你的项目中不起作用的话!我预计许多代码库已经可以很好地使用它,从而能够在毫秒内重建你的项目。当然,如果你遇到任何错误,请提出问题

如果你目前坚持使用 Zig 的标记版本,别担心——正如 Andrew 在他上一篇开发日志中提到的,Zig 0.17.0 即将到来,所以你很快也能尝试这个了!


构建系统重做

作者:Andrew Kelley

大分支刚刚合并:将 maker 进程与 configurer 进程分离

这篇开发日志条目本质上是即将发布的版本说明的预览,但作为对那些想要帮助测试新功能并提供指导 Zig 项目未来发展方向反馈的人的预先通知。

以前,build.zig 文件加上构建系统实现都被编译成一个臃肿的进程,以 Debug 模式运行。在 build.zig 逻辑完成在内存中构建构建图后,“构建运行器”代码执行它。

现在,build.zig 文件被编译成一个小的进程(“configurer”),以 debug 模式运行。在此逻辑完成在内存中构建构建图后,它被序列化为一个二进制配置文件。父 zig build 进程知道这个文件并将其缓存以备下次使用。在等待所有这些的同时,它异步地以 release 模式编译构建图执行进程(“maker”)。一旦配置文件可用并且 maker 进程编译完成,就执行 maker 进程,并将配置文件传递给它。由于全局缓存,maker 进程每个 zig 版本只需要编译一次。然后 maker 进程执行包含在序列化配置文件中的构建图。

此更改的主要动机是使 zig build 更快,通过三种方式:

  1. 只有用户的 build.zig 逻辑会在每次更改时重新编译,而不是整个构建系统。随着我们引入了 --watch--fuzz--webui,这开始变得更有价值。构建系统可以增加更多功能,而不会使 zig build 花费更长时间。
  2. 现在,当构建系统知道没有任何变化时,它可以完全跳过重新运行 build.zig 逻辑,例如,如果你向 zig build 命令行添加 -freference-trace,它现在会避免冗余重新运行你的 build.zig 逻辑,而是使用与上次相同的配置。
  3. 现在,实际执行构建图的进程是启用优化编译的。

为了演示第 2 点和第 3 点,以下是运行 zig build --help 前后的差异:

Benchmark 1 (34 runs): master/zig build -h
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           150ms ± 5.52ms     145ms … 165ms          4 (12%)        0%
  peak_rss           84.8MB ± 275KB     84.2MB … 85.1MB         0 ( 0%)        0%
  cpu_cycles          593M ± 4.01M       588M … 608M            2 ( 6%)        0%
  instructions        995M ± 52.5K       995M … 995M            0 ( 0%)        0%
  cache_references   25.8M ± 165K       25.4M … 26.1M           0 ( 0%)        0%
  cache_misses        651K ± 20.1K       619K … 697K            0 ( 0%)        0%
  branch_misses       918K ± 7.44K       906K … 935K            0 ( 0%)        0%

Benchmark 2 (348 runs): branch/zig build -h
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          14.3ms ± 744us     13.2ms … 23.3ms         8 ( 2%)  ⚡- 90.4% ±  0.4%
  peak_rss           78.5MB ± 562KB     77.1MB … 81.4MB         7 ( 2%)  ⚡-  7.4% ±  0.2%
  cpu_cycles         24.1M ± 821K       22.8M … 27.1M           3 ( 1%)  ⚡- 95.9% ±  0.1%
  instructions       43.7M ± 23.8K      43.7M … 43.8M          56 (16%)  ⚡- 95.6% ±  0.0%
  cache_references   1.46M ± 14.6K      1.40M … 1.50M          19 ( 5%)  ⚡- 94.3% ±  0.1%
  cache_misses        142K ± 4.87K       127K … 157K            2 ( 1%)  ⚡- 78.1% ±  0.4%
  branch_misses       126K ± 1.37K       120K … 129K           12 ( 3%)  ⚡- 86.3% ±  0.1%

这很显著,因为以前,build.zig 逻辑在每个 zig build 命令中都会被执行,但现在,构建系统使用缓存的序列化配置代替。

除了性能之外,我期望第三方工具(如 ZLS)能够从使用序列化配置文件而不是维护构建运行器的分支中受益。

这个变更集大量重写了 zig 构建系统的内部机制,然而,从 API 的角度来看,它大部分是不破坏的,除了上面链接的 PR 中指出的例外。

对于大多数人来说,我猜这是他们会遇到的主要破坏性变更:

if (b.args) |args| {
    run_cmd.addArgs(args);
}
⬇️
run_cmd.addPassthruArgs();

这从构建脚本中移除了一项能力,因为它们无法再观察这些参数。作为交换,这意味着当更改这些参数时,构建脚本不再必须从源代码重新构建。

如果你是想要影响 Zig 发展方向的人,现在是时候将你的项目升级到开发版本并尝试这些更改了。我们将在几周内发布 0.17.0。但是,如果你没有时间,并且发现 0.17.0 破坏了你的构建,别担心,0.17.1 标签也会有足够的机会来修复问题。


使用 LLVM 进行增量编译

作者:Matthew Lugg

在上个月合并了我的类型解析更改后,我花了一些时间在个人项目上,但我最近确实抽出时间对 LLVM 代码生成后端进行了一些改进。这涉及几个不同目标的增强,但一个不错的面向用户的更改是我设法让增量编译与 LLVM 后端一起工作。

遗憾的是,这无法加速可怕的 LLVM Emit Object:那段时间完全取决于 LLVM。然而,增量编译有助于最小化在实际 Zig 编译器代码中花费的时间,这意味着如果你的代码有编译错误(因此“LLVM Emit Object”将被跳过),你通常很快就会得到这些错误。(当然,它在成功构建时也会给你带来轻微的速度提升。)

此支持现在在 master 分支构建中可用,并将包含在 0.16.0 版本中(我们将很快标记)。

对于任何尚未尝试过的人,特别是如果你正在使用 Zig 的 master 分支,请通过向 zig build 传递 -fincremental --watch 来尝试增量编译!Zig 核心团队在我们的工作流程中受益于增量编译已经整整一年了,我们也听到了用户的好评。该功能目前相对稳定,人们常常惊讶于他们可以通过在毫秒而不是秒内获得最新的编译错误来节省多少时间。

我个人并没有真正使用过 LLVM 后端的增量编译,但现在 CI 中的所有增量测试覆盖都已为 LLVM 后端启用,并且我收到了用户的积极反馈,所以绝对值得一试。一如既往,如果你在增量编译中遇到错误,请尽可能报告它们!

谢谢,我希望你觉得这有用 :)


类型解析重新设计,附带语言更改

作者:Matthew Lugg

今天,经过两个(可以说是三个)月的工作,我合并了一个 30,000 行的 PR。这个分支的目标是将 Zig 编译器的内部类型解析逻辑重新设计为更合乎逻辑和直接的设计。对我来说,这是一个非常令人兴奋的更改,因为它让我清理了一堆编译器内部结构,但它也有一些你可能感兴趣的面向用户的更改!

一方面,Zig 编译器现在在分析类型的字段时更加懒惰:如果该类型从未被初始化,那么 Zig 就没有必要关心该类型“看起来像什么”。当你有一个同时充当命名空间的类型时,这很重要,这是现代 Zig 中的常见模式。例如,当使用 std.Io.Writer 时,你不希望编译器也拉入 std.Io 中的一堆代码!这是一个简单的例子:

const Foo = struct {
    bad_field: @compileError("i am an evil field, muahaha"),
    const something = 123;
};

comptime {
    _ = Foo.something; // `Foo` 仅用作命名空间
}

以前,这段代码会发出编译错误。现在,它编译得很好,因为 Zig 实际上从未查看过 @compileError 调用。

我们做的另一项改进是在“依赖循环”体验方面。以前在 Zig 中遇到依赖循环编译错误的任何人都知道,它们的错误消息完全没有帮助——但现在情况变了!如果你遇到一个(现在也比以前更不可能),你会得到一个详细的错误消息,准确告诉你依赖循环来自哪里。看看这个:

const Foo = struct

原文出处:https://ziglang.org/devlog/2026/

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

Zig LLVM 编译器 bitCast 编程语言
← 上一篇
SOCKMAP——未来的TCP拼接技术
下一篇 →
DSpark:置信度调度投机解码——半自回归生成实现推理加速60-85%

📌 相关推荐

结构化输出 vs 工具使用 vs 预填充:2026年从Claude获取JSON
2026/7/11
在AWS Bedrock上构建生产级AI代理——值得关注的架构与代码决策
2026/7/11
如何为你的AI代理构建评估框架(确保其在生产环境中不崩溃)
2026/7/11
← 返回文章列表