1 ポイント 投稿者 GN⁺ 5 시간 전 | 1件のコメント | WhatsAppで共有
  • Zig の master ブランチに、LLVM バックエンドにおける非 ABI 整数処理の改善と新しい @bitCast セマンティクスがマージされ、最適化の問題と言語動作の不一致があわせて整理された
  • u4i13u40 のような 任意ビット幅整数は、SSA 値では bit-int として扱いつつ、メモリ保存時には ABI サイズの整数へ拡張する方式に変更された
  • 従来の @bitCast はメモリ上のバイト再解釈に近かったが、新しい定義は型の 論理的なビット配列を基準に解釈し、エンディアン依存性を減らす
  • 変更は LLVM・C バックエンドと comptime 実行にまで拡張され、標準ライブラリ・コンパイラ・compiler_rt の関連する利用箇所もあわせて点検された
  • 見逃されていた LLVM 最適化が復活したことで、Zig コンパイラ自体で約 5% の性能改善が観測され、0.17.0 では一部のランタイム性能向上が期待できる

LLVM バックエンドにおける任意ビット幅整数処理の変更

  • Zig は従来、u4i13u40 のような 任意ビット幅整数型を、LLVM IR の bit-int 型である i4i13i40 へ直接 lowering していた
  • この方式は LLVM のメモリ表現セマンティクスが最適化器に不要な制約を作り、Clang がこのような LLVM IR を生成しないため、LLVM 内部の経路も十分にテストされていなかった
  • 過去数年で、実際に 最適化の見落とし誤コンパイルの事例が観測されている
  • 新しい方式では、SSA 値の操作には bit-int 型を維持しつつ、メモリに保存するときは i8i16i32 のような ABI サイズ型へ zero-extend または sign-extend する
  • この lowering は、C の _BitInt(N) を Clang が lowering する方式と一致しており、LLVM でよりよくサポートされる経路になると期待される

従来の @bitCast の限界

  • 従来の @bitCast は概念的に次の動作に近かった
    • オペランド値のポインタを取得する
    • そのポインタを目的の型のポインタへキャストする
    • そのポインタから値をロードする
  • つまり従来の定義は、型の論理構造よりもメモリの バイト再解釈に近かった
  • 時間が経つにつれて実際の動作はこの定義から外れ、多くのターゲットで @sizeOf(u24)@sizeOf([3]u8) より大きいにもかかわらず、[3]u8u24@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]u8u16@bitCast すると、従来のセマンティクスでは対象のエンディアンによって結果が異なっていた
    • big-endian ターゲットでは、1 つ目の配列要素が上位 8 ビットになる
    • little-endian ターゲットでは、1 つ目の配列要素が下位 8 ビットになる
  • 新しいセマンティクスは論理的なビット表現だけを考慮するためエンディアンに依存せず、すべてのターゲットで 1 つ目の配列要素が下位 8 ビットになる
  • 一般的には、little-endian ターゲットでの従来の動作により近い
  • [2]u3@Vector(3, u2) に変換するような非定型の変換も可能である
    • 配列の論理ビットを連結した後、2 ビット単位で読み取ってベクター要素を構成する
    • 整数を @Vector(n, u1)@bitCast し、個々のビットベクターへ分解する用途にも使える

あわせて反映された提案と移行

  • 今回の作業中に、@bitCast に関連する小さな受理済み提案もあわせて実装された
    • ポインタベクターとの @bitCast 禁止: #18936
    • enum に対する @bitCast 許可: #35602 の一部
  • 新しいセマンティクスは従来のセマンティクスと意味のある違いがあるため、標準ライブラリ、コンパイラ、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件のコメント

 
GN⁺ 5 시간 전
Lobste.rsの意見
  • 記事でいう 論理的ビット表現 はエンディアン非依存だとされているが、実際の説明はビッグエンディアンのビット順やバイト順をサポートしていない、明らかに リトルエンディアン な方式に見える

    • ここでいう エンディアン非依存 とは、リトルエンディアン/ビッグエンディアンのアーキテクチャ間で動作が変わらない、という意味に見える
  • 2026年6月25日付の新しい開発ログで、新しい @bitCast セマンティクス とLLVMバックエンドの改善が 最近のプルリクエスト にマージされたという内容

  • 興味深いが、めったにテストされない ビッグエンディアン対象 では、以下のように書かれたコードが突然壊れることはないだろうかと思う
    Zig以外の疑似コードで書くと:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • 私もそう思ったが、結局のところ避けられない変更を先延ばしにしても問題が大きくなるだけだと思う
      実際には大きな問題ではなさそうで、Zigリポジトリ内の 数千件の @bitCast のうち、この変更の影響を受けたものは100件よりかなり少なかったようだ
      正直なところ、配列/ベクタとスカラの間の変換で @bitCast がどう動作するかを、ほとんどのZigユーザーが正確に理解していたとも思わない。これまでは作者のシステムでしかテストされずリトルエンディアンでしか動かなかったコードが、今ではどこでも動くようになるケースも多そうだ
  • 昔のCプログラマとしては、Cの ビットフィールド はアーキテクチャごとに動作の移植性がなく、あまり人気がなかったと記憶している
    新しいZigの @bitCast セマンティクスは、異なるアーキテクチャでも同じ結果を与える 移植可能な抽象セマンティクス なので、まさに必要だった方向だと思う
    最近、自分の言語でビットフィールドとビットキャストの設計をしているので、自分のコードがどう動くべきかを明確にするために、Zigの設計と実装のドキュメントをもっと詳しく見てみるつもりだ

    • Cのビットフィールドに対するZigの主な代替は、おそらく packed structpacked union で、どちらも新しい @bitCast の定義とうまく噛み合うように定義されている
      packed struct はフィールドのビットを「基盤整数」に詰め込む方式だ。たとえばフィールドが boolu6i9 で、基盤整数が u16 なら、u16 の最下位ビットが bool、次の6ビットが u6、残りの9ビットが i9 になる。つまりZigのpacked structは、複数のシフトとマスクの上に載ったシンタックスシュガーに近い
      packed union も基盤整数を持つが、すべてのフィールドが基盤整数と正確に同じビット数を使わなければならない。したがって、あるフィールドに格納して別のフィールドから読む動作は、新しいセマンティクスの @bitCast とほぼ同一だ。ただし packed union / packed struct のフィールドは配列やベクタ型を持てない
      個人的には、こうしたツールは「ビット関連の構造」を表現するのにとても向いていると思う。複数の値を packed struct でビットパックしてCのビットフィールドのように使えるし、ビット演算の上にあるシンタックスシュガーなので、Cでは型安全でないマクロの寄せ集めで処理していた ビットフラグ もきれいに表現できる
      たとえばRWXアクセスフラグは、Cでは ACCESS_READACCESS_WRITEACCESS_EXEC マクロと uint8_t APIで受け取るかもしれないが、Zigでは Access = packed struct(u8) として readwriteexecreserved フィールドを定義し、APIで Access を受け取れる
      packed structpacked 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.stabswitch すればよく、そうでなければ n_type.bits の他のフィールドを見ればよい。逆に .{ .stab = .gsym }.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } } のように値を作ることもできる
      元記事の話題とは別の言語機能の話になって少し長くなってしまったが、新しい言語設計の参考になるものを探しているなら、Zigの packed structpacked union を実際に使ってみるとよいと思う。シンプルだが、かなり良いツールだと思う