概要
- C言語 をターゲットとしたコンパイラ生成についての実践的な知見
- static inline関数 によるデータ抽象化と最適化
- 明示的な型変換 と意図を持ったラップ構造体の活用
- ABIや戻り値処理の工夫 によるCコード生成の安定化
- Cをターゲットとする利点と課題 の整理
Cをターゲットとしたコンパイラ生成の実践ノート
- C言語 はアセンブラより高レベルなターゲット言語としてよく利用される選択肢
- 自動生成されたCコード は手書きCより未定義動作の回避が容易
- 生成パターン や設計の工夫で堅牢なCコード生成を実現
static inline関数によるデータ抽象化
- static inline 関数はデータ抽象化のコストをゼロにできる最適化手段
- かつては マクロ が多用されたが、データアクセスや実装隠蔽にはinline関数が適切
- 例:WebAssemblyのメモリ範囲をstructで管理し、アクセス関数もstatic inlineで記述
- BOUNDS_CHECK などのチェックもマクロで柔軟に制御
- static inline によって、structの値渡し時のABI制約やパフォーマンス懸念も解消
暗黙の整数変換を避ける
- C言語 のデフォルト整数変換(例:uint8_t→int)や符号付き整数の境界条件は厄介
- 生成Cコードでは 明示的な型変換関数 (例:u8_to_u32, s16_to_s32等)を用意
- -Wconversion フラグの活用で変換ミスを検出
- 型変換関数をstatic inline化し、型保証と最適化の両立
意図を持ったラップ構造体で生ポインタや整数を包む
- Whippet (C製GC)の例:size_tやuintptr_tのまま扱うと混乱しやすい
- gc_ref, gc_edge 等の一要素structで概念ごとに型を分離
- 型ごとに適用可能な操作を限定し、誤用を防止
- コンパイラ生成時も型安全性をCコードに持ち込める
- WebAssemblyの型階層をCのstruct継承で再現
- 例:type_0ref, structref, eqref等のサブタイプ構造体
- static inline関数でキャストやフィールドアクセスを型安全に実装
memcpyの活用を恐れない
- WebAssembly のメモリアクセスはアラインされていないことが多い
- memcpy を使って値をロードし、最適化はCコンパイラに任せる
- アドレスを型キャストして直接参照するより安全かつ高効率
ABIと多値戻りでの手動レジスタ割り当て
- attribute((musttail)) でtail callが可能になったが、引数や戻り値が多い場合はCコンパイラのABI管理が不安定
- 全パラメータをレジスタに割り当てるため、最初のn個はレジスタ、残りはグローバル変数で受け渡し
- 多値戻りもグローバル変数経由で実現可能
- 呼び出し側・被呼び出し側で値のロード・ストアを明示的に制御
Cをターゲットとする利点と課題
- GCCやClang の強力な命令選択・レジスタ割り当てを享受
- 既存のCランタイム関数との連携や最適化も容易
- デメリット :
- スタック管理の自由度が低い(必要スタック量の把握や拡張が困難)
- ゼロコスト例外処理の実装が困難(コンパイラ・ツールチェーンの支援必須)
- ソースレベルデバッグ情報(DWARF等)の埋め込みが難しい
- Rust をターゲットにする案もあるが、元言語が明示的なlifetimeを持たない場合はメリットが薄い
まとめと所感
- C生成はローカル最適解 :最小限の実装コストで高性能な出力を得やすい
- 型安全性や抽象化 も工夫次第で十分担保可能
- 完璧な手法は存在しないが、 生成Cの型チェックが通ればほぼ動作 する安心感
- 現場の知見を活かし、今後も最適な生成手法を探求する姿勢
いくつかの実験やおもちゃを通じて、ほとんどの投稿に同意するよ。must_tail属性が主要な3つのコンパイラで信頼できるものになればいいんだけど、今のところそれは期待できないね(幸いにも、最近のClangはWindowsでかなり信頼できるみたい)。追加で2つのポイントがあるんだけど、1つ目は、記事がDWARFについて触れているけど、これがなくても#lineディレクティブを使って生成されたコードに行番号を付けられるよ(デバッグの際にすごく役立つ)。もう1つは、ローカル変数とその内容について。変数については、C++のサブセットを使うことでかなりの距離を稼げるよ(コンパイル時間に影響しないサブセット、つまりstd::名前空間のインクルードは避ける)。例えば、"root/gc/smart"ポインタなど(言語のセマンティクスによるけど)、#lineディレクティブがあればデバッガーに変数が表示されるから、「まともな」名前のマングリングが必要だね。2つ目は、Cをバックエンドにしたときの本当の痛点はGCだよ。最良のGCは通常のスタックフレームと絡み合っているから、普通のスタックウォーキングルーチンも正確なGCに必要な情報を提供するんだ(移動GC設計には必須、もっと単純な世代コレクタはそれなしでも可能だけど)。もし正確でそこそこ速いポータブルなスタックスキャンが必要なら、現在の最も理にかなった方法はシャドウスタックを維持することだね。呼び出しの際に前のフレームポインタを渡して、前のフレームポインタはフラットな配列の末尾を指すポインタで、マジックポインタと前の前のフレームポインタを前に追加して(いくつかの書き込みコストと、クリーンアップコストなしの追加引数でリンクリストを形成する)。残念ながら、パフォーマンスの良いリンクドシャドウスタックはデバッグのためにすべてのポインタを隠してしまうから、複数の名前付き変数ではなく1つの配列にまとめる必要があるし、スタック上の複雑なオブジェクトを制限されるんだ。新しいC++のリフレクションサポートを使ってシャドウスタックを利用できればいいけど、コンパイル時間が壊れないことを願ってるよ。でも、それはまた別の話だね。