@bitCast 新语义与 LLVM 后端改进
摘要
Zig 语言引入了新的 @bitCast 语义,并通过更改整数降低(integer lowering)来避免编译错误,并更好地与编译器优化对齐,从而改进了其 LLVM 后端。
<p><a href="https://lobste.rs/s/uge7mm/new_bitcast_semantics_llvm_backend">评论</a></p>
查看缓存全文
缓存时间: 2026/06/25 13:12
# 开发日志 ⚡ Zig 编程语言
来源:https://ziglang.org/devlog/2026/?2026-06-25
本页面包含 main 分支 Zig 近期变更的精选列表。
本页面收录的是 **2026** 年的条目。其他年份可在 Devlog 存档页面 (https://ziglang.org/devlog/) 找到。
---
2026 年 6 月 25 日
## 新的 @bitCast 语义与 LLVM 后端改进 (https://ziglang.org/devlog/2026/?2026-06-25#2026-06-25)
作者:Matthew Lugg
(这篇开发日志有点长,见谅——我有点写上头了!)
几周前,我开始在一个分支上工作,实现一项计划已久的 LLVM 后端改进。这最终像滚雪球一样变成了一项更大的改动,实现了一些你可能感兴趣的语言提案。
## LLVM 后端整数降级
Zig 一直将任意位宽的整数类型(例如 `u4`、`i13`、`u40`)直接降级为 LLVM IR 的 bit-int 类型(`i4`、`i13`、`i40`)。然而,我们早就知道这种降级并非最优,因为 LLVM 对这些类型的内存表示方式的文档化语义对优化器施加了不必要的限制。或许更重要的是,由于 Clang 从未像这样生成 LLVM IR,LLVM 中的这些代码路径从未得到过充分测试,因此在实际中支持得很差——在过去几年中,我们观察到许多本应显而易见的优化被错过 (https://github.com/ziglang/zig/issues/17768),甚至直接出现错误编译 (https://codeberg.org/ziglang/zig/issues/35560)。
因此,PR 的原始目标是仅在 SSA 形式中操作值时使用这些 bit-int 类型,而在存储到内存时将其零扩展或符号扩展到 ABI 大小类型(`i8`、`i16`、`i32` 等)。这应该能得到很好的支持,尤其是因为它与 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 (https://github.com/ziglang/zig/issues/19755),旨在通过精确指定一套新的语义来解决 `@bitCast` 的问题。该提案提交后不久就被接受了,事实上,它所详述的语义已经由自托管的 x86_64 后端实现了!因此,为了解决 LLVM 后端的问题,我不一定需要匹配旧的 `@bitCast` 语义——这似乎是一个很好的时机,来最终在所有地方实现新的语义。
顺便说一句,这样做还有一个好处:我们可以利用编译器的 `Legalize` 过程,它将难以降级的操作重写为更简单的操作,这样编译器后端只需要支持这些简单的操作。`Legalize` 已经具备了由自托管 x86_64 后端使用的功能,可以将复杂的 `@bitCast` 操作转换为更简单的操作,并且可以很容易地适应于帮助其他编译器后端(主要是 LLVM 和 C 后端)——但前提是它们实现了新的语义。
无论如何,重点是我开始了一项支线任务(结果比原始任务还难)——在整个编译器中实现这些新语义。这不仅包括 LLVM 和 C 后端,还包括 `comptime` 执行——毕竟,Zig 允许你在编译时执行几乎任何操作,包括 `@bitCast`!由于新语义与旧语义有显著不同(稍后会详述),我还必须审计标准库、编译器和支持库(例如 `compiler_rt`)中大量 `@bitCast` 的使用。不过在 CI 失败中经历了一些基本无痛的修复后,我终于让我的 PR (https://codeberg.org/ziglang/zig/pulls/35711) 变绿,并于昨天合并到了 master 分支(过程中还关闭了好几个 issue!)。
## 新的 `@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)` 来实现。
在处理所有这些事情的同时,我还实现了一些较小的已接受提案——这里不详细说明,但如果你感兴趣,可以看看这些 issue:
- 禁止从/向指针向量进行 `@bitCast` (#18936 (https://github.com/ziglang/zig/issues/18936))
- 允许对枚举进行 `@bitCast` (属于 #35602 (https://codeberg.org/ziglang/zig/issues/35602) 的一部分)
当然,所有这些改变的语义都将在 0.17.0 发布说明中解释(希望比我现在写得更简洁!),并概述建议的迁移步骤。
## LLVM 后端性能
最后,我只想提一下,这个分支最初的动机——改变 LLVM 后端降级非 ABI 整数类型的方式——已经证明成功地恢复了被错过的优化 (https://github.com/ziglang/zig/issues/17768#issuecomment-4787726124)。事实上,Zig 编译器本身——尽管内部没有大量使用任意位宽整数——从更好的优化中看到了大约 5% 的性能提升。这意味着在 0.17.0 中,你可能会期待一些小的运行时性能提升!
感谢阅读,希望你们中的一些人觉得这很有趣。快乐编码!
---
2026 年 5 月 30 日
## ELF 链接器改进 (https://ziglang.org/devlog/2026/?2026-06-25#2026-05-30)
作者:Matthew Lugg
过去几周我一直在研究我们的新 ELF 链接器,它在 Zig 0.16.0 中首次亮相。在 0.16.0 发布时,这个链接器实现还处于相当早期的阶段,并且只真正支持链接纯 Zig 代码,没有任何外部库(甚至连 libc 都没有)——这就是为什么它当时(现在仍然)默认是禁用的(可以通过 `-fnew-linker` 启用)。不过,自那次初始发布以来,已经取得了相当大的进展!
这里有一个里程碑——根据我最近的 PR (https://codeberg.org/ziglang/zig/pulls/35533),新的 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 的 Tetris 克隆 (https://github.com/andrewrk/tetris) 上进行操作的片段:
对 Andrew 的 Tetris 克隆进行一些愚蠢的更改,每次构建大约需要 30ms。哦,快速增量重建在 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 调试信息——这绝对是我接下来的优先事项。但即使没有这种支持,立即重建的价值也非常巨大,例如在你进行大量打印调试的任何情况下。
如果你正在使用 master 分支的 Zig,并且使用 x86_64 Linux,请考虑尝试使用新的 ELF 链接器进行增量编译——如果你之前的项目不适用的话!我预计许多代码库已经可以很好地使用它,从而能够在毫秒级重建你的项目。当然,如果你遇到任何错误,请务必提交 issue (https://codeberg.org/ziglang/zig/issues)。
而且,如果你目前坚持使用 Zig 的标签发布版本,不用担心——正如 Andrew 在上次开发日志中提到的,Zig 0.17.0 即将发布,所以你很快也能尝试这个功能了!
---
2026 年 5 月 26 日
## 构建系统重做 (https://ziglang.org/devlog/2026/?2026-06-25#2026-05-26)
作者:Andrew Kelley
大型分支刚刚合并:将 maker 进程与 configurer 进程分离 (https://codeberg.org/ziglang/zig/pulls/35428)
这篇开发日志条目基本上是即将发布的发布说明的预览,但也是给那些希望帮助测试新功能并提供指导 Zig 项目未来发展的反馈的人的提前通知。
以前,`build.zig` 文件加上构建系统实现都被编译成一个臃肿的进程,以 Debug 模式运行。在 `build.zig` 逻辑完成构建图的内存构建后,“构建运行器”代码执行它。
现在,`build.zig` 文件被编译成一个小的进程(“configurer”),以调试模式运行。在该逻辑完成构建图的内存构建后,它被序列化为一个二进制配置文件。父 `zig build` 进程知道这个文件,并将其缓存供下次使用。在等待所有这些的同时,它异步地编译构建图执行进程(“maker”),以发布模式运行。一旦配置文件可用并且 maker 进程编译完成,就执行 maker 进程,并将配置文件传递给它。由于全局缓存,maker 进程每个 `zig version` 只需要编译一次。然后 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.
相似文章
Zig 构建速度正在提升
Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。
Zig ELF 链接器改进开发日志
新的 Zig ELF 链接器现在支持外部库和 C 源码的快速增量编译,在 x86_64 Linux 上能够实现毫秒级重建。
构建系统重构
Zig 构建系统已经重构,将配置器和制造器进程分离,支持缓存、发布模式编译,并且'zig build'命令速度提升高达90%。这一变化提高了性能,并允许构建系统在不减速的情况下增加功能。
用 Zig 写一个 C 编译器
一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。
Bun 的 Rust 重写已合并
Bun,JavaScript 运行时和包管理器,已合并其核心从 Zig 到 Rust 的重写,可能提升性能和可维护性。