RocコンパイラのRust→Zig書き換え進捗記
(rtfeldman.com)- 既存実装の構造的欠陥を解消するため、Rust 30万行をZigで書き直し、487日で機能同等性に到達。今年後半の最初の正式番号付きリリースである0.1.0を目指す
- 新コンパイラはホットコードローディングと再現可能なクロスコンパイル、パターンマッチ内の文字列補間、HTTPルーティングにおけるヒープ割り当ての排除をサポートし、
Rocci Birdのwasmサイズも半分以下の31KBに削減 - Zigを選んだ主な理由は、ビルド時間、細かく制御できるアロケータとデータ配置、コンパイラ開発に適したエコシステム、メモリ非安全コードの検査であり、Zig 0.17.0のインクリメンタルビルドは約46万行を35msで再ビルドする
- 実際のバグ分類では、Rustコンパイラはメモリ破損が21件、Zigコンパイラは10件だったが、その大半は誤ったコード生成によるもので、Zigコンパイラ自体のメモリ安全性エラーはファイル名を壊したuse-after-free 2件だった
- Zigはポインタなしのデータ構造、パース不要のデシリアライズ、LLVM bitcodeシリアライザの再利用に適していた一方、テストでの自動メモリ解放、ポリモーフィズム、非公開構造体フィールド、デッドコード検出、リリース間の互換性ではRustの開発体験のほうが優れていた
機能同等性に到達した書き直し
- Rocコンパイラチームは約1年半をかけてRust 30万行をZigで書き直し、既存コンパイラとの機能同等性に到達した
- 機能同等性は重要なマイルストーンだが正式リリースではなく、新コンパイラの0.1.0は今年後半を目標としている
- 2024年のWASM-4ゲームRocci Birdを新コンパイラでビルドできるようになった
- ホームページでは、2.5MBのWebAssemblyコンパイラを使って基本的なRocプログラムをブラウザ上で作成・実行できる
- 書き直しには487日かかり、BunがZig約50万行をRustへ移した11日より476日長かった
- Bunは直接的な移植だったが、Rocは複数のコンパイラ構造と機能を大きく変える、書き直しに近いものだった
- そのため、2つのコードベースの規模や期間をRustとZigだけで直接比較するのは難しい
新コンパイラが提供する開発体験
-
ホットコードローディングとクロスコンパイル
roc server.rocでサーバを実行したあとコードを修正すると、次のリクエストから新しいコードが自動適用される- Pythonのようなインタプリタ言語では一般的だが、Rocのような高性能コンパイル言語では珍しい挙動
- Webサーバだけでなく、簡単な2Dゲームでも同じ方式で動作する
- デプロイ時には、
roc build server.rocがLLVM最適化を適用したスタンドアロンバイナリを生成する roc build --target=x64muslでAlpine Linux向けの静的バイナリをクロスコンパイルできる- 同一のソースバイトを入力すれば、Macを含むどのビルドシステムでも同一の出力バイトを生成する
-
パターンマッチ内の文字列補間
"/users/${id}"のような文字列補間をパターンマッチ内でサポートする- Express式ルーティングのように実行中にテンプレート文字列をパースするのではなく、コンパイラが直接処理する
- HTTPメソッドとパスを一緒にマッチでき、ネストしたパスやデフォルト値もパターンで分岐できる
- 例のHTTPリクエスト処理コード全体は、コンパイル時に型安全性を検査しながらヒープ割り当てを一切行わない
- 純粋関数のコンパイル時実行を利用してHTTPリクエストルーティングを無割り当てで実装しており、ホームページのWebAssemblyコンパイラで構文を試せる
最初から書き直した理由
- RocはRust・C・Zigとは異なりシステム言語ではなく、参照カウントベースの自動メモリ管理を使う
- トレーシング方式ガベージコレクタの一時停止を避ける
- Perceus最適化とKoka式の機会的変更を活用する
- 一般的な非システム言語のようにクロージャのキャプチャごとにヒープを割り当てず、ラムダ集合特殊化による多相的非関数化を使う
- 非関数化は関数型言語においてインライン化のように複数の後続最適化を可能にするが、既存実装を正確にするのは非常に難しかった
- Ayaz HafizのOCamlプロトタイプを通じて、問題が複数のコンパイラ段階にまたがる構造的欠陥であり、解決するにはコンパイラの大部分を書き直す必要があることを確認した
- 他のコントリビューターもそれぞれ異なる理由で複数の部分を書き直す計画だったため、ほぼ全体を段階的に置き換える「テセウスの船」方式ではなく、全面的な書き直しを検討することになった
- 成功したコンパイラプロジェクトでは、セルフホスティングなどのために最初から書き直す例がよくあり、自前の言語ではなくGoへ移したTypeScriptの書き直しの例もある
- Rocコンパイラはセルフホスティングしない方針だが、今回は書き直しで得られる利点が既知のコストを上回ると判断した
RustではなくZigを再び選んだ基準
- チームはすでに標準ライブラリの複数の基本機能でZigを使っており、十分に理解しているシステム言語がRustとZigだけだったため、この2言語だけを真剣に検討した
- プロジェクトごとに適した言語は異なり、Rustを他の仕事で使い続けることとRocにZigを選ぶことは矛盾しない
-
ビルド時間
- Rustの
cargoビルドはインクリメンタルビルドでも時間がかかり、コードベースが大きくなるにつれて主な不満として残った - Zigへ移行すればビルドが大幅に速くなると予想した
- Rustの
-
メモリとデータ配置の制御
- コンパイル段階ごとにさまざまなアロケータ、特にアリーナアロケータを使い、構造体配列(SoA)配置を広範に活用する
- Rustエコシステムは概ね単一のグローバルアロケータを前提にしているが、Zigエコシステムは細かく分けたアロケータの受け渡しを基本とし、SoAサポートも標準で提供する
-
必要なエコシステムの範囲
- Rust全体のエコシステムはZigより大きいが、Rocの特殊な要件に関連するパッケージはどちらのエコシステムにも多くなかった
- LLVM C++ライブラリをラップする代わりに高速にbitcodeを生成するコードのような、必要なニッチ機能はZig側により多かった
-
メモリ非安全コードのサポート
- 既存のRustコンパイラ30万行には約1,200個の
unsafe使用があった - 比較対象として、Rustコンパイラ自体は350万行で約4万個の
unsafeを使っている - 機械語を生成するコンパイラでは、メモリ非安全な作業が重要な業務の一部となる
- Rustはまれな
unsafeブロックを隔離し、MiriやValgrindで検査するモデルだが、Rocではunsafeは珍しくなかった - Zigはメモリ非安全コードを正しく動作させるための機能をより多く提供し、チームはまさにこの領域で最も多くの支援を求めていた
- 既存のRustコンパイラ30万行には約1,200個の
borrow checkerなしで得たメモリ安全性
-
RustとZigの検査範囲
- Microsoftの2019年の資料によると、毎年セキュリティアップデートで処理される脆弱性の約70%がメモリ安全性の問題だった
- 同資料の2018年の分類をRustとZigの観点で分けると次のようになる
- 83.6%は範囲外読み書き、安全でないキャスト、未初期化読み取り、スタックオーバーフロー、非メモリ安全性問題で、RustとZigの選択には影響されないカテゴリである
- 16.4%はuse-after-freeであり、Rustのborrow checker、Zig
ReleaseSafe、Fil-C方式の検査で検出できるカテゴリである - Zigの
ReleaseSafeは、解放済みメモリを使用すると実行中にパニックを発生させる- Rustの安全なサブセットほど検査は包括的ではない
- 実行コストがあり、プログラムがパニックする可能性がある
ReleaseFastは本番環境では検査を省略するが、デバッグビルドとテストでは維持する- すべての実際の実行経路をテストできれば
ReleaseSafeと同様の安全性を得られるが、そのようなテスト範囲は一般的には現実的ではない
-
他のZigプロジェクトとRustの
unsafe- TigerBeetleは
ReleaseSafeを使用しており、精密なJepsen検証で安全性バグが2件発見されたが、どちらもメモリ安全性とは無関係だった ReleaseFastを使用するGhosttyとZigコンパイラにも、Zigコードのメモリ非安全性によって発生したCVEはない- Rustプログラムでも依存関係内部の
unsafeを通じてメモリ安全性の空白が生じることがある - Unsafe Rustには
ReleaseFastZigと同じリスクがあるが、開発中に問題を検出するZigの実行時検査に相当する機能はない - MiriとValgrindは役立つ可能性があるが、これらを使っているRustプロジェクトは多くない
- 代わりに
unsafeをまれにしか使わず、より厳格にレビューする文化が、実務におけるRustの強いメモリ安全性の評判を作っている - Rustベースのプロジェクトでも
unsafe関連の脆弱性が発生した事例がある- Denoには範囲外読み取りとuse-after-freeのCVEがあった
- Rocketにはuse-after-freeのCVEがあった
- Actixでは
unsafeの使用量が異常に多かった時期に、複数のメモリ非安全性CVEが発生した
- Rocでは割り当ての大半が寿命の単純なアリーナで行われるため、use-after-freeを大きなリスクとは見なさず、Zigの追加検査は本質的に非安全なコードに対してより有用だと判断した
- TigerBeetleは
書き換え後に確認したメモリ破壊バグ
- Claude Opus 4.8でRocのIssueトラッカーを分類した結果は次のとおり
| コンパイラ | メモリ破壊発生 | メモリ破壊なし | 合計 |
|---|---|---|---|
| Rust | 21 | 2,575 | 2,596 |
| Zig | 10 | 421 | 431 |
- Rustコンパイラのメモリ破壊21件は、コンパイラ内部ロジックの破壊ではなかった
- borrow checkerは意図した役割を果たしていた
- 誤って生成された機械語が、コンパイルされたプログラムでメモリを破壊するミスコンパイルのバグだった
- Zigコンパイラのメモリ破壊10件のうち8件もミスコンパイルだった
- 残り2件はエラー報告コードのuse-after-freeだった
roc checkとroc bundleのエラーメッセージで、ファイル名がU+FFFD置換文字に化けた- Rustのborrow checkerであれば、2つのバグをどちらも検出できた
- 別のツール選択が実際のユーザーにもたらした結果は次のとおり
| ツール選択 | 実際のメモリ安全性への影響 |
|---|---|
Zig ReleaseFast |
一部のエラーメッセージでファイル名がレンダリングされないバグ2件 |
Zig ReleaseSafe |
一部のエラーがパニックし、ファイル名をレンダリングできないバグ2件 |
| Rust borrow checker | 2つのバグをどちらも防止 |
- 18か月、数十万行、数百件のバグ報告を考えると、3つの選択肢の差はプロジェクトにとって実質的には大きくなかった
- BunはJavaScriptのトレーシングGC値と手動管理値を一緒に扱うため、use-after-free、double-free、解放漏れが大きな割合を占めていた
- RocコンパイラはJavaScriptや他のトレーシングGCと連携しないため、同じ寿命管理問題は経験しない
- Rocには、コンパイラ内部のメモリエラーよりも、生成された出力コードのメモリエラーを見つけるツールがより必要であり、後者はborrow checkerの範囲外である
ビルド時間の実際の結果
zig build --watch -fincrementalは現在、約45万〜46万行のZigコードで変更点を約35msで再ビルドする- 安定版Zig 0.16.0には、Rocコードベースで
-fincrementalを壊すバグがある- 修正は取り込まれているが、これを使うには互換性が破れるZig 0.17.0の事前ビルドへ移行する必要がある
- 関連依存関係も一緒にベンダリングし、0.17.0へアップグレードする必要があるため、次の安定リリースを待つことにした
- IntelデスクトップとUbuntu 26で測定した結果は次のとおり
| Rocコンパイラ | コード規模 | コールドビルド | インクリメンタルビルド |
|---|---|---|---|
| Rust 1.85.0ベースの元版 | 354K | 32.4秒 | 10.0秒 |
| Rust 1.97.0ベースの元版 | 354K | 25.4秒 | 3.4秒 |
| Zig 0.16.0の機能同等時点 | 320K | 39.6秒 | 8.6秒 |
| Zig 0.17.0の現在の書き換え版 | 464K | 32.1秒 | 0.035秒 |
- 機能同等時点ではほとんど変わらない成果物も毎回再ビルドしていたが、現在は必要なときだけ生成する
- この変更でコードが約50%増えたにもかかわらず、コールドビルドは速くなった
- Rust 1.85.0から1.97.0へ移行する過程で、インクリメンタルビルドは10秒から3.4秒へ短縮され、18か月で約3分の2改善した
- Zigの35msは3.4秒の約100分の1であり、Rustの測定コードより約50%大きいコードベースで得られた結果である
- 現在
-fincrementalはx86-64でのみ動作し、多くのコントリビューターがARMベースのMacを使っているため、ビルド時間の利点をまだ十分には活用できていない
ポインタを持たない構造とパース不要のデシリアライズ
- 新しいディスクキャッシュは、Zig コンパイラやゲーム開発で使われる手法のように、実行時に効率的なメモリ配置をそのままディスク形式として使う
- すべてのコンパイラのデータ構造は、ポインタの代わりに 32ビットのインデックス配列で表現され、多くの箇所で struct の配列形式を使う
- メモリ使用量を減らし、実行速度を高める
- 別のシリアライズ形式に変換せず、データ構造をディスクへ直接書き込める
- デシリアライズ時にはディスク上のバイト列をパースしない
- バイト列をメモリに読み込む
- 既存のデータ構造が新しい配列を指すように一部を再配置する
- その後すぐに利用できる
- 速度は実質的に、ディスクからメモリへバイト列を読む I/O 速度に制限される
- OS のディスクキャッシュにデータがあれば、おおむね
memcpyの速度で以前のビルド結果を読み込む
- OS のディスクキャッシュにデータがあれば、おおむね
roc checkは初回実行時に、パースや型検査などの結果をディスクに保存する- 入力ソースが変わっていなければ、2回目の実行ではデータ構造をディスクからメモリへ直接移す
roc testは決定的な純粋関数テストの結果もキャッシュする- キャッシュはファイル単位で動作するため、1つのファイルを変更すると、そのファイルと依存ファイルだけを再処理する
- この方式は、コンパイラ全体がポインタではなくインデックスを使っているから可能であり、一般的なポインタ中心の構造では パース不要のデシリアライズは不可能
-
インデックスベース構造の安全性の限界
- ポインタが誤ったアドレスを指し得るのと同様に、インデックスも誤った配列を参照して任意のバイトを読み取る可能性がある
- Rust の borrow checker はポインタの寿命を扱うが、どのインデックスがどの配列に属するかは検査しない
- 必要な配列数が事前に分かっているなら、Rust の
compact_arenaがマクロで型タグを作り、誤った配列参照を防げる - Roc のようにモジュール数によって配列数が変わる場合、この手法は適用できず、
compact_arenaもSmallArena::newをunsafeとしている - 空の arena を作ること自体は危険を生まないが、実際の危険は非常に頻繁に行う配列インデックス操作にある
- Safe Rust は
unsafeが小さく隔離されているという前提で有効だが、Roc のようにunsafeが広範囲に及ぶ場合、この前提は成り立たない
Roc に合っていた Zig エコシステム
- Bun では JavaScript と手動メモリ管理の相互運用のため、クリーンアップコードを一度だけ実行する Rust の
Dropが有用だった - Roc は反対に、モジュールやコンパイル段階ごとに 別々の arena を使おうとしているため、グローバルアロケータと暗黙の
Dropを前提とした Rust パッケージは扱いにくかった - Zig エコシステムではアロケータを明示的に渡す API が一般的で、Roc のメモリ管理方式とよく合っている
- Rust エコシステムは Bun が望む構造に、Zig エコシステムは Roc が望む構造に、それぞれより適していた
-
LLVM bitcode シリアライザの再利用
- LLVM は Roc オプティマイザの中核的な依存先であり、Roc 自身の最適化の後に追加の最適化を行う
- LLVM は主要 API の互換性を頻繁に壊すため、バージョンアップには相当な時間とコストが必要だった
- LLVM のシリアライズされた bitcode 形式は安定しており後方互換性があるため、独自のシリアライザを使えば C++ API の変更から離れられる
- そのためには、LLVM C++ ライブラリから分離された手作業の bitcode シリアライザが必要になる
- 既存の既知の実装は Zig コンパイラにあり、Roc の新しいコンパイラはその Zig コードを再利用している
- Roc が Zig エコシステムから得ている最大の依存性の源泉は、一般的なパッケージよりも Zig コンパイラ自体である
Zig で恋しくなった Rust の機能
- コンパイラ実装では明示的なアロケーション制御が必要だが、テストでは Rust の 自動アロケーション・解放のほうが便利だった
- Zig のテストアロケータはメモリリークを検出し、コンパイルされた Roc コードのリークも検知できる
- その代わり、各テストで
initとdefer deinitを正確に書く必要があり、1つでも間違うとリークとしてテストが失敗する
- Zig の
comptimeはパラメトリック多相やアドホック多相と重なるが、この2つの多相が恋しくなる- Rust の
Allocatortrait はselfを受け取れる - Zig の
ArenaAllocatorのような実装はanyopaqueポインタを受け取った後、自分の型へキャストしなければならない
- Rust の
- 非公開 struct フィールドがないため、直接アクセスしてはいけないフィールドをコンパイルエラーで防げない
- コード差分レビューではフィールドアクセスだけが見え、元の struct のドキュメントは見えないため、その都度別途確認する必要がある
- 関数・変数・定数がすべて
snake_caseを使う Rust の一貫性が、ときどき恋しくなる unsafeと borrow checker にはコストもあったが、特定の問題をunsafeブロックの中だけで心配すればよいという安心感があった- Zig に同じ機能を追加すべきだという立場ではない
- Zig では Rust よりも デッドコードを後から見つけることが多かった
- Zig の組み込みツールや TigerBeetle の
tidy.zigでも、一部のデッドコードを検出できない - デッドコードはバイナリに生成されないためユーザーには影響しないが、コードベースの管理には不利である
- Zig の組み込みツールや TigerBeetle の
- Rust はマイナーバージョンアップやエディション変更の大半が容易だった
- 現在の Zig は後方互換性を目標としておらず、想定していた条件なので大きな問題ではなかったが、Rust の簡単なアップグレード体験のほうが優れていた
Zig で満足した点
- Zig には、関数型プログラミングのように慣れ親しんだ道具を減らす代わりに別の特性を得る 減算的設計の魅力がある
- マクロがなく、パラメトリック多相を含む多くの問題を
comptimeや通常の関数で解決できる - データ配置を細かく制御できる
u7、u5のように2の累乗ではない整数型を、別途ビット処理なしに使える- packed struct を標準でサポートする
- 宣言位置ではなく呼び出し位置で関数をインライン化できる
- Rust ではマクロベースのクレートが必要な機能を、追加依存なしで利用できる
- Zig のビルドツールチェーンは、Alpine Linux と WebAssembly 向けの スタンドアロンバイナリ生成によく合っていた
- Roc 標準ライブラリに相当する builtins を不透明なバイナリ blob としてコンパイルし、最終実行ファイルに含める特殊なビルドも処理する
- Uber も Zig 言語を使わずに、ビルドインフラで Zig ツールチェーンを使用している
- Zig のエラー処理では、失敗したヒープアロケーションも通常のユーザー空間エラーとして扱う
- Roc も匿名 sum type とペイロードを用いて、エラーが自然に蓄積される類似の戦略を使う
- Rust の
anyhow、thiserror、標準のResultベースの処理よりも、Zig と Roc の方式を好んでいる - エラー伝播構文は Zig の
tryより Rust の後置?を好み、Roc にも 後置?演算子を採用した - きめ細かなアロケータ API と高性能コンパイラ向けの再利用コードを含め、プロジェクト全体としては Zig の選択に非常に満足している
Roc の次のステップ
- 新しいコンパイラの 0.1.0 を今年後半にリリースする計画で、Roc 初の番号付きリリースとなる
- リリース前でも Nightly ビルド を試せるが、現時点では複数のバグ、未完成の機能、不完全なドキュメントが残っている
- Roc Programming Language Foundation は米国の 501(c)(3) 非営利団体で、寄付金は主に貢献者への報酬に使われる
- 今後の開発状況や質問は Roc Zulip で確認できる
2件のコメント
Lobste.rs の意見
数値上は Zig の 35ms は Rust の 3.4秒よりほぼ100倍速く、コードも約50%多いが、実際の開発ではその差が誇張されているように思える
コンパイラを毎分リビルドするのではなく10分ごとにビルドするなら、節約できる時間は約3秒で、3.4秒でも十分速い
コードベースが大きくなると Rust が遅くなる可能性はあるが、コールドビルド時間はむしろ悪化しており、2年ごとにノートPCを買い替えることやコンパイラ自体の改善まで考えると、Rust のビルド時間が長期的に増え続けると断定するのは難しい
Rust の
checkモードと LSP はフル再コンパイルより速いが、同じ水準ではない--watch -fincrementalの組み合わせを使えば、保存するたびに再コンパイルされ、非常に高速かつ頻繁にフィードバックを得られる個人プロジェクトを Rust から Zig に移し、Rust も何年も仕事で使ってきたが、Zig のコンパイル速度は本当に息がつけるような変化だった
35ms とは驚くほど速い
再リンクだけでもそれより長くかかりそうだが、コンパイラが実際に何をしているのか気になる
関数の実装を変更した場合は、新しいアセンブリを既存バイナリに挿入すればよいが、インライン化、関数シグネチャ変更、依存関係、空き領域不足時の再配置などには追加の解析が必要
関数を修正または追加すると、出力実行ファイルの
.textセクションで十分に大きい空き領域を探し、新しい機械語を書き込む空間が足りなければ
.textを拡張し、他のデータを移動する必要があるかもしれないが、セクションを指数的に拡張してコストを分散する必要ならシンボルテーブルと再配置エントリも追加し、それらのための空き領域もあらかじめ残してあるため、作業後にファイルを閉じれば完了する
最近見た Tracy の結果を基準に推定すると、全体の35msのうちリンカーが使った時間は 約1msにすぎない
30万行の Rust コードで
unsafeを約1,200回使っているという数字は、予想より多い例えば Inko には
unsafe { ... }式が162個、C ABI 経由で生成コードに公開されるunsafe関数が87個しかなく、Rust コード全体は約 8万8千行ただし Inkwell の動作方式のため、LLVM バックエンド全体が事実上巨大な
unsafe領域になっており、単純比較で結論を出すのは難しい私はポインタを直接扱うような、実際にメモリ安全性を損なう関数だけを
unsafeと表示するが、他の開発者は、パニックを避けるには特定の作業を先に行う必要があるというヒントとして表示することもあるRust 標準ライブラリとコンパイラで
unsafeが 4万回登場するという数字は不正確テストやコメントに出てくる単語まで数えた結果であり、その大半は標準ライブラリにあり、コンパイラ自体ではコメントやテストを含めても2千回未満
rustc に貢献していた間、私の PR のうち
unsafeを含む割合は1%にも満たないほど、コンパイラ内部の安全でないコードはまれだったRust 全体が依存する基礎ランタイムを実装する標準ライブラリに
unsafeが多いのは自然であり、こうしたコードはどの言語でも明示的であれ暗黙的であれ安全ではない当該コンパイラは rustc より約10倍小さいのに
unsafeの使用量は似ているため、蔓延しているとまでは言いにくいが頻繁に遭遇することになり、rustc よりはるかに多く必要な理由が気になる両方のコードベースで標準ライブラリ関連の
unsafeを個別に分析したわけではないが、新しいコンパイラを Rust で実装していたなら、キャッシュやポインタの代わりにインデックスを使う構造のため、rustc よりunsafeが多かったと予想しているRust チームの仕事を貶める意図はなく、他プロジェクトの成果を尊重しつつ、私たちの選択と進捗を明らかにしようとしたものだった
RustとZigそれぞれの長所と短所を隠さずバランスよく評価した記事であり、18か月にわたって数十万行を書き、数百件のバグを処理した後でも、選択肢の中で別の言語を選んでいたとしてもプロジェクトの結果が大きく変わることはなかっただろう、という結論が特に率直
ただし、Rocやrustcのように機械語を生成するコンパイラで、メモリ非安全な作業が業務の大きな部分だという説明は理解しにくい
OCamlやHaskellで書かれたコンパイラも多く、機械語の生成自体はバイト列をベクタに組み立ててファイルに書き出す作業なので、安全でない理由はない
インタプリタやJITコンパイルなら理解できるが、通常のコンパイルにも必要な理由が気になる
危険は生成された機械語を実行するときに生じるもので、実際のコンパイラでは機械語を生成すると同時に実行する場合が多いため、業務の大きな部分と表現した
インタプリタやJIT以外にも、Rustの
const fnやRocでトップレベルに持ち上げられる式のように、コンパイル時にユーザーコードを評価したり、テストを実行した後に出力を検査してユーザーに表示する内容を決めたりする作業などが該当する破損したメモリがコンパイラプロセス内にあるか生成されたプログラム内にあるかにかかわらず、プロセッサが誤った動作をした原因はコンパイラが作った命令であり、修正すべき場所もコンパイラコードだという基準である
外部リンクコードやガベージコレクタのような構成要素はRustでも
unsafeを多く必要とし、安全なRustの利点をかなり相殺するコンパイラだけを安全な言語で書くことはできるが、性能が問題であり、ZigとRocコンパイラは**構造体配列(SoA)**およびポインタの代わりに配列インデックスを広く使っている
これをRustで実装すると借用チェッカーを迂回することになり、関連する安全性上の利点を失う
コンパイラのライフタイム関係は意外に単純で、ある段階でアリーナにデータを割り当て・生成・修正し、次の段階に読み取り専用で渡した後、アリーナ全体を破棄できる
より複雑なのは、ディスクから状態を読み込み、バイナリをその場で修正するインクリメンタルコンパイルとリンクであり、この過程では状態の破損・バグ・誤ったコンパイル・メモリ問題を引き起こし得るが、コンパイラプロセス自体のメモリ安全性とは別の話である
安全性と正確性は、プログラム内部のメモリ安全性よりはるかに広い概念であり、特にRustでは表現しにくい作業を安全かつ正確に行う場合はなおさらである
compact_arenaの作者として紹介されたのはうれしいが、このライブラリにおけるunsafeの使い方が誤って描写されているcompact_arenaの目的は、安全なRustでnewを誤用できないよう保証することで、インデックス指定を安全にすることであるmk_arenaマクロは、必要なアリーナ数が分からないループ内でも、安全なRustコードから呼び出せるnewが安全に使われるという説明を理解できていなかったのかもしれないが、https://docs.rs/compact_arena/0.5.0/… の安全性セクションには、コンストラクタに渡すタグがインデックス指定手法の基盤であり、この値を別のアリーナで使うと2つのアリーナのインデックスが混ざり、範囲外アクセスと未定義動作につながり得ると書かれている私の要点は
new()呼び出し自体ではなく、new()で作った値にインデックス指定するときに危険が発生するということであり、監査してunsafeとして示すべき箇所も実際のインデックス指定の呼び出し側だと考えているただし、設計を誤解している可能性はある
Pythonは新しいコードを反映するにはプログラムを再起動する必要があり、ホットリロードには非標準の拡張機能と慎重なコード作成が必要だと理解している
ホットリロードはLispやSmalltalkのようなイメージベースの開発環境、またはErlangでより一般的である
importlib.reload()を使えばPythonでもホットリロードは可能だが、どれほど広く使われているかは分からないcargo buildを使わなければビルド時間を短縮できるのか気になるBazelを構成する作業がZigへ移植する作業より大きいのか小さいのかは不確かであり、もちろんビルド時間だけが言語移行の理由だったわけではない
Hacker Newsの意見
記事全体は悪くないが、Rocやrustcのように機械語を生成するコンパイラにおいて、メモリ安全でない作業が業務の大きな部分だという表現には同意しにくい
実行中のバイナリパッチやコードの再ロードにはunsafeなコードが必要だろうが、普通に実行ファイルを作る過程で、機械語生成そのものがunsafeである理由はない
むしろ言語のランタイムでunsafeなコードを見つける可能性のほうが高い
ただ実際には、多くのコンパイラが機械語を生成しながら直接実行もするので「業務の大きな部分」と表現したが、コンパイラが必ず両方の作業をしなければならないわけではない
バイナリパッチ、コードの再ロード、ランタイムだけでなく、Rustの
const fnやRocでトップレベルに持ち上げられる式のように、コンパイル時にユーザーコードを評価すること、テストを実行して出力を検査し表示内容を決める作業などを念頭に置いていたシートベルトをしていても頭をぶつけることはあるのだから、シートベルトは面倒なだけで使うべきではない、という論理に似ている
極端な高性能コードでは、データ構造やアルゴリズム、メモリ割り当て戦略からして異なり、TigerBeetleは起動時にメモリを一括で全量割り当てることで知られている
RocコンパイラもZigに近いトレードオフを選ぼうとしているため、いくつもの共通パターンが見つかるのは自然だ
ReleaseSafeがランタイム検査で**解放後使用(use-after-free)**を捕捉するという主張には根拠が見当たらないZigのドキュメントでランタイムのメモリ安全検査を調べたが、
use-after-free、UaF、safety-checkedで検索しても関連する内容は見つからず、リリースビルドでDebugAllocatorを使ってもこれを安定して検出できるわけではない関連する記事はhttps://landaire.net/memory-safety-by-default-is-non-negotia...にまとめた
AI以前に自分でコードを書いて得た判断であり、ZigとLLMの組み合わせでは何かが変わる可能性もあるが、記事を読むほど奇妙に表現された主張がさらに見えてきて、信用しにくくなる
率直な技術的判断というより、既存の論争を正当化するために書かれた文章のように読める。ただ、変わった記事や言語は好きだし、過剰なAIブームにも反感があるので、ひとまず善意に解釈したい
ReleaseSafeは境界チェックを追加し、到達不能コードでパニックを起こす程度だと理解しているZigは時間的メモリ安全性(temporal memory safety)を提供していないように見える
成熟したOCamlが柔軟で表現力も十分なためプロトタイプ検証には使われたが、最終的な実装言語には選ばれなかった点が興味深い
Zigのインクリメンタルビルドがduneより有意に速いのかも疑問で、クロスコンパイルが利点であるにもかかわらず、「なぜZigなのか」では扱われていない
コンパイラに細かなメモリ制御が本当に重要なのか、OCamlで始まったRustやWASMのように、どのマイルストーンでメンテナたちが別の言語へ移行すると決めるのかも気になる
Zigのインクリメンタルビルドは明らかに決定的な機能であり、短期的にそれを得るために言語を変える選択は理解できる
しかし中期的には、Rustにも近い将来似た機能が追加されるのではないかと期待している
速度は欲しいが、急ぎすぎて足元をすくわれたくはない。Rustの安全性、Zigの機能、ガベージコレクションのないGoランタイムを組み合わせた言語を自分で作っている
こちらのほうが実現可能性が高く、ユーザー空間で実装することも可能に見える
コンパイラの実装言語を選ぶ過程で、実際に科学的な比較を行っていれば、はるかに説得力があったはず
高性能なコンパイラには低レベルのシステム言語が必要だという、検証されていない前提(https://www.roc-lang.org/faq#self-hosted-compiler)から出発し、Rust 以外の唯一の選択肢が Zig だと結論づけたように見える
コンパイラの性能はアルゴリズムが支配的であり、高速なマネージド言語でも同じアルゴリズムなら実行時間はおおむね2倍以内に収まる一方、アルゴリズムの違いによる性能差には上限がない
Zig 自体が、低レベル言語でコンパイラを書けば速くなるという理論への反例であり、Roc の毎秒約1万5000行は速いとは言えない。1998年の時点でも ML コンパイラが毎秒3000行を処理していたという資料がある(https://flint.cs.yale.edu/cs421/case-for-ml.html)
現在のコンパイラ作業を止め、1万行未満で実現できる小さな Roc サブセットのセルフホスティングコンパイラを作るほうが、将来的には役立つかもしれない
そうすれば、30万行規模の実装ではなく1万行規模で複数の実装を試し、低レベル言語が実際の性能目標に必要なのかを検証できる
セルフホスティングの過程では、本当に重要な Roc の機能が明らかになり、Roc のコードもより多く書くことになる。コンパイラに必要な汎用機能を改善すれば、その上にあるアプリケーションもあわせて良くなる
1990年代に ML を高速にコンパイルできたという事実だけで、今日の Roc のコンパイル速度を判断するのは難しい。言語設計は必要となるアルゴリズムに強い制約を与え、現代のハードウェアもはるかに複雑だからだ
Roc にはある程度のオーバーロードがあり、クロージャをヒープに割り当てないための精巧なアルゴリズムもあるようで、こうした要件が取り除けないアルゴリズム上の複雑さを生む可能性がある
アルゴリズム最適化の限界に達すると、残るのは定数係数の削減であり、特にメモリを管理する高水準言語では、それをどこまで下げられるかに明確な下限が生じる
実際のコードでメモリ配置を直接制御することで性能が10倍以上改善した例を見たことがあり、ゲーム業界ではこうした作業がキャリアの大きな部分を占めることもある。賢いアルゴリズム一つですべての性能問題が消えるような環境は、現実からは程遠い
Rust のビルドは、あらゆるコンピュータでストレージを大きく浪費する原因の一つであり、複数のライブラリをビルドすると数十 GB がすぐに積み上がる
依存関係をプロジェクト間で再利用するようグローバルビルドフォルダを設定することはできるが、どのような解決策であれデフォルト動作として提供されるほうが望ましい
ただしキャッシュのガベージコレクションは必要で、それを容易にする新しい中間ビルド成果物の配置作業が最終段階にある
node_modulesのサイズが常に批判されていることを考えると、興味深い対比だフロントエンドよりバックエンドコードのほうがはるかに小さい Tauri プロジェクトでも、Rust のビルド成果物は9GBで、
node_modulesはわずか550MBにすぎないRoc を C ABI に組み込むスクリプティング言語として理解しているが、実際の活用分野が気になる
大きな Roc プラットフォームを提供するプラグイン環境で WASM と競合しようとしているのか、プラグイン開発者がどの言語でも使える WASM の代わりに、アプリケーション開発者が Roc レイヤーを公開する理由が何なのか知りたい
小さな Roc プラットフォームを使うアプリケーションレベルの言語なら、サーバー側の HTTP コードでは Gleam、クライアントコードでは Elm と競合しようとしているのかも気になる
コンパイル時間はひどく過小評価されている要素だ。C++ のビルドを10分ずつ待つことが最大の不満で、開発への集中を完全に断ち切ってしまう
.rsファイルから.tsファイルに変えると、コンピュータそのものを買い替えたかのような差を感じるほどだZig を直接使ってはいないが、いくつかの可能性には大いに期待している
C より使いやすい Zig で書かれた新しいゲーム、TigerBeetle ですでに可能性を示している分散ソフトウェア、個人的に関心のあるロボティクス分野には特に期待している