Zig、新しい @bitCast セマンティクスと LLVM バックエンド改善
(ziglang.org)- Zig の master ブランチに、LLVM バックエンドにおける非 ABI 整数処理の改善と新しい
@bitCastセマンティクスがマージされ、最適化の問題と言語動作の不一致があわせて整理された u4、i13、u40のような 任意ビット幅整数は、SSA 値では bit-int として扱いつつ、メモリ保存時には ABI サイズの整数へ拡張する方式に変更された- 従来の
@bitCastはメモリ上のバイト再解釈に近かったが、新しい定義は型の 論理的なビット配列を基準に解釈し、エンディアン依存性を減らす - 変更は LLVM・C バックエンドと
comptime実行にまで拡張され、標準ライブラリ・コンパイラ・compiler_rtの関連する利用箇所もあわせて点検された - 見逃されていた LLVM 最適化が復活したことで、Zig コンパイラ自体で約 5% の性能改善が観測され、0.17.0 では一部のランタイム性能向上が期待できる
LLVM バックエンドにおける任意ビット幅整数処理の変更
- Zig は従来、
u4、i13、u40のような 任意ビット幅整数型を、LLVM IR の bit-int 型であるi4、i13、i40へ直接 lowering していた - この方式は LLVM のメモリ表現セマンティクスが最適化器に不要な制約を作り、Clang がこのような LLVM IR を生成しないため、LLVM 内部の経路も十分にテストされていなかった
- 過去数年で、実際に 最適化の見落としと 誤コンパイルの事例が観測されている
- 新しい方式では、SSA 値の操作には bit-int 型を維持しつつ、メモリに保存するときは
i8、i16、i32のような ABI サイズ型へ zero-extend または sign-extend する - この lowering は、C の
_BitInt(N)を Clang が lowering する方式と一致しており、LLVM でよりよくサポートされる経路になると期待される
従来の @bitCast の限界
- 従来の
@bitCastは概念的に次の動作に近かった- オペランド値のポインタを取得する
- そのポインタを目的の型のポインタへキャストする
- そのポインタから値をロードする
- つまり従来の定義は、型の論理構造よりもメモリの バイト再解釈に近かった
- 時間が経つにつれて実際の動作はこの定義から外れ、多くのターゲットで
@sizeOf(u24)が@sizeOf([3]u8)より大きいにもかかわらず、[3]u8をu24へ@bitCastすることが許されていた - LLVM バックエンドは十分に仕様化されていない
@bitCastセマンティクスを実装しており、整数型のメモリ保存方式を変えると、コンパイラのテストスイートで Illegal Behavior とクラッシュが発生した - LLVM バックエンドに従来の動作をまねるロジックを追加する代わりに、新しい
@bitCast定義を全体的に実装する方向が選ばれた
新しい @bitCast セマンティクス
- 新しいセマンティクスは、2024 年に提出され受理された言語提案 #19755 に基づく
- このセマンティクスはすでに self-hosted x86_64 バックエンドに実装されており、今回の変更で LLVM・C バックエンドと
comptime実行にまで拡張された - 新しい
@bitCastは、メモリ上のバイトではなく、型を 論理的に表現するビット順序を基準に動作するu5は least-significant bit から most-significant bit までの 5 個の論理ビットで構成される[2]u5は、1 つ目の要素の 5 ビットの後に 2 つ目の要素の 5 ビットが続く、10 個の論理ビットで構成される
u8を同じサイズのi8に変える場合のような単純な整数間変換では、ビットはそのまま維持され、最上位ビットが符号ビットとして解釈される- 整数型と
packed structまたはpacked unionの間の@bitCastセマンティクスも維持される
配列・ベクターで変わる動作
- 新しいセマンティクスが従来と異なる点は、配列やベクターのような aggregate 型が関係する場合である
- たとえば
[2]u8をu16へ@bitCastすると、従来のセマンティクスでは対象のエンディアンによって結果が異なっていた- big-endian ターゲットでは、1 つ目の配列要素が上位 8 ビットになる
- little-endian ターゲットでは、1 つ目の配列要素が下位 8 ビットになる
- 新しいセマンティクスは論理的なビット表現だけを考慮するためエンディアンに依存せず、すべてのターゲットで 1 つ目の配列要素が下位 8 ビットになる
- 一般的には、little-endian ターゲットでの従来の動作により近い
[2]u3を@Vector(3, u2)に変換するような非定型の変換も可能である- 配列の論理ビットを連結した後、2 ビット単位で読み取ってベクター要素を構成する
- 整数を
@Vector(n, u1)へ@bitCastし、個々のビットベクターへ分解する用途にも使える
あわせて反映された提案と移行
- 今回の作業中に、
@bitCastに関連する小さな受理済み提案もあわせて実装された - 新しいセマンティクスは従来のセマンティクスと意味のある違いがあるため、標準ライブラリ、コンパイラ、
compiler_rtのようなサポートライブラリでの@bitCast利用が点検された - 関連 PR は codeberg.org/ziglang/zig/pulls/35711 で、master にマージされるとともに複数の issue もあわせてクローズされた
- 変更後のセマンティクスと推奨される移行手順は、Zig 0.17.0 リリースノートにまとめられる予定である
0.17.0 で期待される性能効果
- 当初の目標だった LLVM バックエンドの非 ABI 整数 lowering 変更は、見逃されていた最適化を復活させることに成功した
- 関連する結果は demonstrably successful で確認できる
- Zig コンパイラ自体は内部的に任意ビット幅整数をあまり使っていないにもかかわらず、より良い最適化のおかげで約 5% の性能改善を示した
- 0.17.0 では、一部のコードで小さなランタイム性能向上が生じる可能性がある
1件のコメント
Lobste.rsの意見
記事でいう 論理的ビット表現 はエンディアン非依存だとされているが、実際の説明はビッグエンディアンのビット順やバイト順をサポートしていない、明らかに リトルエンディアン な方式に見える
2026年6月25日付の新しい開発ログで、新しい
@bitCastセマンティクス とLLVMバックエンドの改善が 最近のプルリクエスト にマージされたという内容興味深いが、めったにテストされない ビッグエンディアン対象 では、以下のように書かれたコードが突然壊れることはないだろうかと思う
Zig以外の疑似コードで書くと:
実際には大きな問題ではなさそうで、Zigリポジトリ内の 数千件の
@bitCastのうち、この変更の影響を受けたものは100件よりかなり少なかったようだ正直なところ、配列/ベクタとスカラの間の変換で
@bitCastがどう動作するかを、ほとんどのZigユーザーが正確に理解していたとも思わない。これまでは作者のシステムでしかテストされずリトルエンディアンでしか動かなかったコードが、今ではどこでも動くようになるケースも多そうだ昔のCプログラマとしては、Cの ビットフィールド はアーキテクチャごとに動作の移植性がなく、あまり人気がなかったと記憶している
新しいZigの
@bitCastセマンティクスは、異なるアーキテクチャでも同じ結果を与える 移植可能な抽象セマンティクス なので、まさに必要だった方向だと思う最近、自分の言語でビットフィールドとビットキャストの設計をしているので、自分のコードがどう動くべきかを明確にするために、Zigの設計と実装のドキュメントをもっと詳しく見てみるつもりだ
packed structとpacked unionで、どちらも新しい@bitCastの定義とうまく噛み合うように定義されているpacked structはフィールドのビットを「基盤整数」に詰め込む方式だ。たとえばフィールドがbool、u6、i9で、基盤整数がu16なら、u16の最下位ビットがbool、次の6ビットがu6、残りの9ビットがi9になる。つまりZigのpacked structは、複数のシフトとマスクの上に載ったシンタックスシュガーに近いpacked unionも基盤整数を持つが、すべてのフィールドが基盤整数と正確に同じビット数を使わなければならない。したがって、あるフィールドに格納して別のフィールドから読む動作は、新しいセマンティクスの@bitCastとほぼ同一だ。ただしpacked union/packed structのフィールドは配列やベクタ型を持てない個人的には、こうしたツールは「ビット関連の構造」を表現するのにとても向いていると思う。複数の値を
packed structでビットパックしてCのビットフィールドのように使えるし、ビット演算の上にあるシンタックスシュガーなので、Cでは型安全でないマクロの寄せ集めで処理していた ビットフラグ もきれいに表現できるたとえばRWXアクセスフラグは、Cでは
ACCESS_READ、ACCESS_WRITE、ACCESS_EXECマクロとuint8_tAPIで受け取るかもしれないが、ZigではAccess = packed struct(u8)としてread、write、exec、reservedフィールドを定義し、APIでAccessを受け取れるpacked structとpacked unionを使えば、かなり変わったビット配置も表現できる。Mach-Oオブジェクトフォーマットのシンボルテーブルエントリには、歴史的理由によるものと思われる奇妙なn_typeフィールドがあるが、これをpacked union(u8)の中にbits: packed struct(u8)とstab: enum(u8)という形でモデル化できるこの
n_type値を扱う際に、手動のシフトやマスキングは不要だ。n_type.bits.is_stab != 0を確認し、真ならn_type.stabをswitchすればよく、そうでなければn_type.bitsの他のフィールドを見ればよい。逆に.{ .stab = .gsym }や.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }のように値を作ることもできる元記事の話題とは別の言語機能の話になって少し長くなってしまったが、新しい言語設計の参考になるものを探しているなら、Zigの
packed structとpacked unionを実際に使ってみるとよいと思う。シンプルだが、かなり良いツールだと思う