プログラミング
Zigの新しい@bitCastセマンティクスとLLVMバックエンドの改善
Zig's New BitCast Semantics and LLVM Back End Improvements (ziglang.org)
要約
Zig言語の開発ログでは、LLVMバックエンドにおける任意のビット幅整数型の扱いが改善され、最適化の向上と誤コンパイルの削減が図られたことが報告されています。特に、組み込み関数`@bitCast`のセマンティクスが変更され、メモリの再解釈ではなく論理ビット表現に基づく定義にすることで、エンディアンに依存しない挙動を実現しました。この変更は、より正確で堅牢な型変換を可能にします。
全文翻訳
開発ログ このページには、メインブランチのZigに対する最近の変更の厳選されたリストが含まれています。RSSフィードでも利用可能です。このページには2026年のエントリが含まれています。他の年のエントリは開発ログアーカイブページで利用できます。
2026年6月25日
新しい@bitCastセマンティクスとLLVMバックエンドの改善
著者: Matthew Lugg
(かなり長い開発ログになります、お詫びいたします—今回は少し興奮してしまいました!)
数週間前、私は長い間計画されていたLLVMバックエンドの改善を実装するブランチでの作業を始めました。これは最終的に、皆さんが興味を持つかもしれないいくつかの言語提案を実装する、より大きな変更へと発展しました。
LLVMバックエンドの整数ローカリング
Zigは常に、任意のビット幅の整数型(例:u4、i13、u40)をLLVM IRのビット整数型(i4、i13、i40)に直接ローカリングしてきました。しかし、このローカリングは最適ではないことが以前から分かっていました。なぜなら、LLVMがこれらの型をメモリ内で表現するための文書化されたセマンティクスが、オプティマイザにとって不必要に厳しいためです。おそらくもっと重要なのは、ClangがこのようなLLVM IRを出力しないため、LLVM内のこれらのコードパスが適切にテストされたことがなく、実際にはサポートが不十分であるということです。過去数年間、些細な最適化が見逃されたり、あからさまな誤コンパイルが発生する多くの事例を観測してきました。
そこで、このPRの当初の目標は、SSA形式で値を操作する場合にのみこれらのビット整数型を使用し、メモリに格納する際にはABIサイズの型(i8、i16、i32など)にゼロ拡張または符号拡張することでした。これは、ClangがC言語の_BitInt(N)をローカリングする方法と一致するため、十分にサポートされるはずです!
この変更自体はかなり簡単でしたが、一つの問題にぶつかり、それが深い探求へと私を導きました。
@bitCastの問題点
@bitCastは興味深い組み込み関数です。以前は、以下の操作シーケンスと同等であると定義されていました。
オペランド値へのポインタを取得する
それを宛先型へのポインタにキャストする
そのポインタからロードする
言い換えれば、それは本質的にメモリのバイトを再解釈するためのシンタックスシュガーでした。しかし、時間の経過とともに、私たちはこの定義から逸脱しました。例えば、[3]u8をu24として再解釈するために@bitCastを使用することが許可されるようになりましたが、ほとんどのターゲットでは@sizeOf(u24)が@sizeOf([3]u8)よりも大きいため、上記の定義では不正な動作が発生します。
これまで、LLVMバックエンドは@bitCast組み込み関数のこれらの不十分な指定のセマンティクスを実装していました。しかし、その定義がメモリの再解釈を伴うため、整数型をメモリに格納する方法を変更すると、@bitCastの実装に影響を与え、コンパイラのテストスイートでクラッシュにつながる不正な動作を導入することになりました。
これに対する最も簡単な解決策は、おそらくLLVMバックエンドに古い動作に近似するロジックを実装することだったでしょう。私は代わりに、より良い解決策を選択しました—@bitCastの新しい定義を実装することです。
@bitCastの再定義
2024年、Jacob Youngは@bitCastの問題を正確に新しいセマンティクスを指定することで解決することを目指した言語提案#19755をまとめました。この提案は提出後すぐに承認され、実際、その詳細なセマンティクスはすでに自己ホスト型x86_64バックエンドによって実装されています!したがって、LLVMバックエンドの問題を解決するために、必ずしも古い@bitCastセマンティクスに合わせる必要はありませんでした—むしろ、これは新しいセマンティクスをすべての場所で最終的に実装する良い機会だと考えられました。
余談ですが、これを行うもう一つの利点は、コンパイラのLegalizeパスを利用できることでした。このパスは、ローカリングが難しい操作をより単純な操作に書き換えることで、コンパイラバックエンドがそれらの単純な操作のみをサポートすればよいようにします。Legalizeは、自己ホスト型x86_64バックエンドで使用される機能として、複雑な@bitCast操作をより単純なものに変換する機能をすでに持っており、他のコンパイラバックエンド(主にLLVMとCバックエンド)にも簡単に適応させることができました—ただし、彼らが新しいセマンティクスを実装している場合に限ります。
いずれにせよ、私はサイドクエスト(元々のクエストよりも困難であることが判明しました)に乗り出し、これらの新しいセマンティクスをコンパイラ全体に実装することにしました。これには、LLVMとCバックエンドだけでなく、コンパイル時実行も含まれます—結局のところ、Zigでは@bitCastを含むほぼすべての操作をコンパイル時に行うことができますから!新しいセマンティクスは古いものとは意味的に異なります(これについては後で詳しく説明します)ので、標準ライブラリ、コンパイラ、およびサポートライブラリ(例:compiler_rt)全体で@bitCastの多くの使用箇所を監査する必要もありました。しかし、CIの失敗に対するいくつかのほとんど苦痛のない修正の後、私は最終的にPRをグリーンにすることができ、昨日マスターにマージしました(その過程でいくつかの良い問題を閉じました!)。
新しい@bitCastセマンティクス
これまでの背景を説明し終えたところで、いよいよ新しい@bitCastの動作を実際に説明する時が来ました。以前のようにメモリ内のバイトを再解釈することに基づくのではなく、この組み込み関数は型を論理的に表現するビットの観点から定義されます。
@bitCastをサポートするすべての型には「論理ビットレイアウト」があります—その型を順序付けられたビット列として表現したものです。例えば、u5は5つの論理ビットで構成され、最下位ビットから最上位ビットの順に並べられます。[2]u5は10個の論理ビットで構成されます—最初の要素からの5ビットに続き、2番目の要素からの5ビットが続きます。@bitCastの新しい定義は、ある型の論理ビットを異なる型の論理ビットとして再解釈するというものです。
最も簡単な例は、符号なし整数、例えばu8を取り、同じサイズの符号付き整数、この場合はi8に変換することです。この操作は、期待通りに動作します—ビットは変更されず、最上位ビットを符号ビットとして再解釈するだけです。整数型とパックド構造体/パックド共用体型間の@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);
// Concatenate all bits of `arr` starting with the least-significant bit of `arr[0]` to find the
// logical bit sequence, then read off 2-bit chunks from it to get the elements of the resulting
// vector value `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;`
この種の操作はほとんどの場合あまり有用ではありませんが、必要な場合はそこにあります!例えば、整数を個々のビットのベクトルに分解して操作したい場合など—それは今、@Vector(n, u1)への@bitCastによって行うことができます。
これらすべての作業中に、私はいくつかの小さな承認された提案も実装しました—ここでは詳しく説明しませんが、興味があれば以下のイシューをご覧ください。
ポインタのベクトルへの/からの@bitCastを禁止する (#18936)
enumでの@bitCastを許可する (課題 #35602の一部)
もちろん、これらすべての変更されたセマンティクスは0.17.0のリリースノートで説明され(願わくば、私がここで説明したよりも簡潔に!)、推奨される移行手順が概説されます。
LLVMバックエンドのパフォーマンス