RFC-008:Runtime 並列処理モデルとスケジューラ分離設計
⚠️ 整合に関する注意:本文書はRFC-024 新並列処理モデル と整合しています。旧式の全プログラムDAG分析、
@block/@eagerアノテーション、L1/L2/L3レイヤーモデルはspawn {}ブロック並列プリミティブに置き換えられました。DAG分析は現在はspawn {}ブロック内部でのみ機能します。
参考:
概要
本文書はRuntimeアーキテクチャの重要な設計を定義します:
- ランタイム三層アーキテクチャ:Embedded(即時実行) → Standard(spawn + DAGスケジューリング) → Full(ワークスティーリング)
- コンパイルと実行の分離:コンパイル段階はすべてのモードで同一で、実行時の実行方式のみ異なります
- デュアルバックエンドモデル:VM(開発デバッグ)とLLVM AOT(本番リリース)、動作は完全一致
- スケジューラ = 静的ライブラリ:AOTコンパイル時にスケジューラをexeにリンク、約200-500KB、GCなし
- 同期はスケジューリングの特殊ケース:num_workers=1で同期モード
重要な澄清:これはJavaではない
Java: .java → .class → JVM(解釈/JIT + GC) ← 常に仮想マシンが必要
YaoXiang 開発: .yx → IR → VM実行(高速反復、ステップデバッグ)
YaoXiang 本番: .yx → IR → LLVM → ネイティブexe(スケジューラが静的リンク)
VMは開発ツールであり、ランタイムの本質ではありません。Goのgo run vs go buildと同じです。
最終exe = あなたのネイティブコード + スケジューラ静的ライブラリ + リフレクションメタデータ。
解释器なし、JITなし、GCなし。動機
コアな矛盾
| 矛盾 | 説明 |
|---|---|
| 透明性 vs 制御性 | spawnブロックは明示的な並列制御を提供し、通常のコードは順序実行 |
| コア vs オプション | spawnはコアな並列プリミティブであり、WorkStealingはnum_workers>1の上級機能 |
| シングルスレッド vs 並列処理 | シングルスレッドモードでは並列処理は非同期として振る舞い、同期はスケジューリングの特殊ケース |
提案
1. Runtime三層アーキテクチャ
┌──────────────────────────────────────────────────────────────────┐
│ コンパイル段階(すべてのモードで同一) │
│ │
│ ソースコード → Lexer → Parser → TypeCheck → Codegen → IR │
│ │
│ ⚠️ 同一の構文解析、型チェック、コード生成、IR出力 │
└──────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ 🟢 Embedded │ │ 🔵 Standard │ │ 🟣 Full │
│ 即時実行器 │ │ spawn + DAG │ │ Fullスケジューラ │
│ 同期実行 │ │ spawnブロック内│ │ 並列最適化 │
│ spawnサポートなし │ │ 自動並列処理 │ │ ワークスティーリング│
└──────────────────┘ └───────────────┘ └──────────────────┘| 段階 | Embedded | Standard | Full |
|---|---|---|---|
| コンパイル | 同一 | 同一 | 同一 |
| 実行モード | 同期 | spawnブロック内 | 並列 |
| メモリ使用量 | 低 | 中 | 高 |
| 並列処理能力 | なし | spawnブロック内 | spawnブロック内+並列 |
| spawnサポート | ❌ | ✅ | ✅ |
| DAG分析 | なし | spawnブロック内 | spawnブロック内 |
| WorkStealer | なし | なし | ✅ |
Embedded Runtime:WASM/ゲームスクリプト/ルールエンジン向け。即時実行器、spawnサポートなし、高性能低消費。
Standard Runtime:Webサービス/データパイプライン向け。spawn {} ブロックをサポートし、spawnブロック内でDAG分析と自動並列処理を実行。num_workers=1でシングルスレッド非同期。
Full Runtime:科学計算/大規模並列向け。Standard + WorkStealerによる負荷分散。
2. スケジューラ分離:ジェネリクス + 注入
基本原则:VMは具体的なスケジューラに直接依存せず、ジェネリクスパラメータ[S]を通じて呼び出します。
# スケジューラインターフェース定義
Scheduler: Type = {
spawn: (Task) -> TaskId,
await: (TaskId) -> Result,
spawn_with_deps: (Task, List(TaskId)) -> TaskId,
await_all: (List(TaskId)) -> List(Result),
stats: () -> SchedulerStats,
}
# シングルスレッドスケジューラ
SingleThreadScheduler: Scheduler = {
spawn: (task) => { task_queue.push(task); generate_task_id() },
await: (task_id) => { ... },
spawn_with_deps: (task, deps) => { ... },
await_all: (task_ids) => { ... },
stats: () => { queue_size: task_queue.len() },
}
# マルチスレッドスケジューラ
MultiThreadScheduler: Scheduler = {
spawn: (task) => { work_queue.push(task); generate_task_id() },
await: (task_id) => { wait_for_completion(task_id) },
spawn_with_deps: (task, deps) => { ... },
await_all: (task_ids) => { ... },
stats: () => { workers: get_worker_stats() },
}
# VMはジェネリクスでスケジューラを使用
create_vm: [S: Scheduler](scheduler: S) -> VM = (scheduler) => {
VM(scheduler: scheduler, memory: create_memory(), dag: create_dag())
}コアポイント:
- コンパイル時多相、ゼロ런タイムオーバーヘッド
- traitオブジェクト不要
- ジェネリクス型制約
[S: Scheduler]はRFC-011で既に定義済み
3. 同期 = スケジューリングの特殊ケース
❌ 誤解:スケジューラを無効化
✅ 正しい:単一workerのスケジューラを使用
num_workers = 1 → シングルスレッド非同期スケジューリング
num_workers > 1 → マルチスレッド並列スケジューリング
同一のスケジューラインターフェース、設定が異なるだけ。特殊ケースを排除。4. DAGの地位
重要な変更:DAG分析は全プログラムではなく、
spawn {}ブロック内部のみで機能します。通常コード(spawnブロック外)は順序実行され、DAG分析は不要です。
| レイヤ | spawnサポート | DAG分析範囲 | 説明 |
|---|---|---|---|
| Core Runtime | ✅ | spawnブロック内 | 並列処理コア |
| Standard Runtime | ✅ | spawnブロック内 | spawn + DAGスケジューリング |
| Embedded Runtime | ❌ | なし | 即時実行、並列処理なし |
5. ボトムアップ実行モデル(spawnブロック内)
重要な変更:ボトムアップDAG分析は
spawn {}ブロック内部でのみ実行され、全プログラムに対しては行いません。
ユーザーコード(spawnブロック内並列):
(a, b) = spawn {
fetch(url0),
fetch(url1)
}
print(a)
コンパイル時分析(spawnブロック内ボトムアップ):
fetch(url0) と fetch(url1) は相互依存なし → 並列実行可能
spawnブロック外のprint(a) → 順序実行、spawn完了を待機
ランタイムスケジューリング(spawnブロック内、リーフから):
fetch(url0) ┐
├→ 並列実行
fetch(url1) ┘
print(a) ← spawnブロック外、順序実行コアポイント:
- ボトムアップ依存分析は
spawn {}ブロック内部に限定 - spawnブロック内で依存のないタスクは並列実行
- spawnブロック外のコードは順序実行、spawnブロック完了を待機
6. コンパイルモデル:デュアルバックエンド + 静的リンクランタイム
6.1 二つのバックエンド、一つの動作
┌─────────────────────┐
│ コンパイルフロントエンド(統一)│
│ Lexer → Parser │
│ → TypeCheck │
│ → spawnブロック内DAG分析│
│ → エスケープ分析 │
│ → サイクル検出 │
└──────────┬──────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ VMバックエンド(開発)│ │ LLVMバックエンド(本番)│
│ │ │ │
│ IR/バイトコード生成 │ │ ネイティブコード生成 │
│ VM解釈実行 │ │ ランタイム静的ライブラリ │
│ ステップデバッグ対応 │ │ .exe出力 │
│ 高速反復 │ │ ゼロ解釈オーバーヘッド │
└───────────────────┘ └───────────────────┘
│ │
▼ ▼
動作完全一致 動作完全一致VMバックエンド:開発時に使用。コード修正 → 即時実行 → ステップデバッグ → 高速反復。動作は最終exeと完全一致。
LLVMバックエンド:リリース時に使用。AOTコンパイルでネイティブコード生成、スケジューラは静的ライブラリとしてリンク。解释器なし、JITなし。
6.2 スケジューラ = 静的ライブラリ、仮想マシンではない
最終exeの内部構造:
┌────────────────────────────────────────────┐
│ あなたのコード(ネイティブ機械語) │
│ ├── コンパイル時に確定したDAG実行計画 │
│ ├── インライン化されたMove/ref/clone操作 │
│ └── RAII解放コード │
├────────────────────────────────────────────┤
│ ランタイム静的ライブラリ(~200-500KB) │
│ ├── スレッドプール(固定サイズ = num_workers)│
│ ├── イベントループ(libuv / io_uring) │
│ ├── ワークスティーリングキュー(Full Runtimeのみ)│
│ ├── メモリアロケータ(jemalloc / mimalloc)│
│ └── リフレクションメタデータ(オンデマンド読み込み、常駐なし)│
├────────────────────────────────────────────┤
│ なし: │
│ ❌ バイトコード解释器 │
│ ❌ JITコンパイラ │
│ ❌ GC │
│ ❌ 仮想マシン │
└────────────────────────────────────────────┘比較:
| 言語 | Java | Go | YaoXiang |
|---|---|---|---|
| コンパイル成果物 | バイト码 | ネイティブコード | ネイティブコード |
| 実行方式 | JVM解釈/JIT | 直接実行 | 直接実行 |
| ランタイムサイズ | ~200MB(JVM) | ~1-2MB(GC含む) | ~200-500KB(GCなし) |
| メモリ管理 | GC | GC | RAII(决定的) |
| リフレクション | 常駐メモリ | 常駐メモリ | exe中に保存、オンデマンド |
6.3 なぜスケジューラ性能が一定なのか
重要な洞察:ほとんどの作業はコンパイル時に完了し、ランタイムでは「実行」のみを行います。
コンパイル時(一度だけ、ランタイムには含まれない):
├── spawnブロック内DAG分析:誰が何に依存するか
├── トポロジカルソート:spawnブロック内実行順序確定
├── 並列実行可能タスク識別:spawnブロック内の依存なしサブツリー
├── エスケープ分析:ref → Rc か Arc か
├── サイクル検出:自動降格Weakまたはエラー
└── インライン:小関数は直接展開
ランタイム(実行のたびに、データ構造は固定):
├── コンパイル時に確定したspawnブロックDAG順序でスレッドプールにタスク配布
├── I/O遭遇 → 現在のタスクを一時停止、イベントループが接管
├── タスク準備完了 → 準備完了キューに戻す
└── 以上。スケジューラ自体は固定サイズのデータ構造:スレッドプール、イベントループ、ワークキュー。動的成長なし、アダプティブ再最適化なし、GCスキャンなし。動作は完全に予測可能。
コンパイル時にspawnブロック内の「何をスケジューリングするか」を既に計算済みで、ランタイムでは「実行」のみを行います。これはtokioとは異なり、tokioはランタイムで動的にFutureチェーンを構築します。YaoXiangのDAGは静的であり、spawnブロック内に限定されます。
6.4 リフレクション:保存するが常駐させない
リフレクションメタデータはコンパイル時に生成され、exeの独立セクション(section)に保存されます。プログラム起動時に読み込みません。リフレクションが初めてリクエストされたとき、オンデマンドでmmapしてメモリに読み込みます。例えるなら:
exeレイアウト:
.text ← あなたのコード
.rodata ← 定数
.reflect ← リフレクションメタデータ(型情報、関数シグネチャ等)
mmapでオンデマンド読み込み、不アクセスならメモリ使用なしトレードオフ:exeサイズが増加(リフレクションデータを含む)が、ランタイムでアクセスしなければメモリコストゼロ。初回アクセス時に読み込み遅延あり(JITウォームアップと同様)、以降はゼロコスト。
src/
├── lib.rs
├── main.rs
├── backends/ # ランタイムバックエンド
│ ├── common/ # 全バックエンド共有(値、ヒープ、オペコード)
│ │ ├── allocator.rs
│ │ ├── heap.rs
│ │ ├── opcode.rs
│ │ └── value.rs
│ ├── dev/ # REPL + デバッガ
│ │ ├── debugger.rs
│ │ ├── shell.rs
│ │ └── repl/
│ ├── interpreter/ # 🟢 ツリーランタイム解释器(旧Embedded/VM)
│ │ ├── ffi.rs
│ │ ├── frames.rs
│ │ ├── registers.rs
│ │ ├── runtime.rs
│ │ └── executor/
│ └── runtime/ # 🔵 コンパイル型VMランタイム
│ ├── engine.rs
│ ├── facade.rs
│ └── task.rs
├── frontend/ # コンパイルフロントエンド(全バックエンド共有)
│ ├── compiler.rs
│ ├── config.rs
│ ├── pipeline.rs
│ ├── core/
│ │ ├── lexer/
│ │ ├── parser/
│ │ ├── typecheck/
│ │ │ ├── checker.rs
│ │ │ ├── spawn_placement.rs # ★ spawnブロック内DAG/並列分析(元frontend/dag/)
│ │ │ ├── inference/
│ │ │ └── traits/
│ │ └── types/
│ ├── events/
│ ├── module/
│ └── pipeline/
├── middle/ # ミドルエンド
│ ├── core/ # IR & バイトコード
│ │ ├── bytecode.rs
│ │ ├── ir.rs # IR定義(VMとLLVMで共用)
│ │ └── ir_gen.rs
│ └── passes/ # コンパイルパス
│ ├── codegen/ # コード生成(元codegen/)
│ ├── lifetime/ # ライフタイム/借用分析
│ └── mono/ # 単態化
├── lsp/ # Language Server
├── formatter/ # ソースフォーマッタ
├── package/ # パッケージマネージャ
├── std/ # 標準ライブラリ
│ ├── concurrent.rs
│ ├── io.rs
│ ├── list.rs
│ ├── math.rs
│ ├── net.rs
│ ├── string.rs
│ └── weak.rs
└── util/ # ユーティリティライブラリ
├── diagnostic/
├── i18n/
└── config/ディレクトリマッピング説明(旧 → 新):
| 旧ディレクトリ | 新所在地 | 説明 |
|---|---|---|
frontend/dag/ | frontend/core/typecheck/spawn_placement.rs | spawnブロック内DAG分析が型チェックに統合済み |
codegen/ | middle/passes/codegen/ | コード生成が中間パスの手に移動 |
embedded/ | backends/interpreter/ | ツリーランタイム解释器 |
runtime/ | backends/runtime/ | コンパイル型VMランタイム |
vm/ | backends/interpreter/ | embeddedと統合 |
full/ | (未実装) | Full Runtime + ワークスティーリング、後続バージョンで |
reflect/ | (未実装) | リフレクションメタデータ、後続バージョンで |
core/ | backends/common/ | 共有値/ヒープ/オペコード |
トレードオフ
利点
- 明確な階層化:Embedded / Standard / Full の三層
- コンパイル再利用:フロントエンドコードは完全に共用
- ジェネリクス分離:コンパイル時多相、ゼロオーバーヘッド
- 一貫性:同步はスケジューリングの特殊ケース
- 組込み用途に優しい:高性能 + 低メモリ + 高速起動
欠点
- 初期複雑さ:スケジューラインターフェースと多様なランタイム変体を定義する必要がある
- コンパイル時バインディング:スケジューラ型はコンパイル時に確定
設計意思決定記録
| 意思決定 | 決定 | 日付 |
|---|---|---|
| スケジューラ分離方案 | ジェネリクス + 注入 | 2025-01-05 |
| シングルスレッドモード | 同步はスケジューリングの特殊ケース | 2025-01-05 |
| 非同期実装 | DAGは自然にサポート | 2025-01-05 |
| WorkStealer | Full Runtimeの上級機能 | 2025-01-05 |
| 組込み設計 | 即時実行、DAGスケジューリングなし | 2025-01-05 |
| コンパイル段階 | 全ランタイムで同一フロントエンドを共有 | 2025-01-05 |
| ランタイム階層化 | Embedded / Standard / Full | 2025-01-05 |
| 型制約 | RFC-011で既に定義 | 2025-01-25 |
| 依存グラフ構築 | 静的依存グラフ、コンパイル時に確定 | 2025-01-05 |
| デュアルバックエンドモデル | VM(開発デバッグ)+ LLVM AOT(本番)、動作一致 | 2026-05-11 |
| スケジューラ形態 | 静的ライブラリとしてexeにリンク、~200-500KB、GCなし | 2026-05-11 |
| リフレクションメタデータ | exeの独立セクションにコンパイル、mmapでオンデマンド読み込み | 2026-05-11 |
| スケジューラ性能 | コンパイル時にDAG分析完了、ランタイムは実行のみ | 2026-05-11 |
| DAG範囲整合 | DAG分析はspawnブロック内に限定、RFC-024と整合 | 2026-06-05 |
| 三層アーキテクチャ更新 | Embeddedはspawnなし、Standardはspawnサポート | 2026-06-05 |
参考文献
ライフサイクルと归宿
| 状態 | 所在地 | 説明 |
|---|---|---|
| 草案 | docs/design/rfc/ | 作者草稿 |
| 審査中 | docs/design/rfc/ | コミュニティ議論公開 |
| 受理済み | docs/design/accepted/ | 正式設計ドキュメント |
| 拒否済み | docs/design/rfc/ | RFCディレクトリに保存 |
