VM内部構造
Lambda Cが組み込みシステムで安全である理由
このページでは、Lambda C VMアーキテクチャの技術的深掘りを行い、なぜセーフティクリティカルな組み込みシステムに適しているのかを解説します。
非再帰ディスパッチループ
核心となる安全保証
重要な理解: Lambda CのVMは、スクリプト関数呼び出しにC言語レベルの関数再帰を使用しません。これがナイーブなインタープリタとの根本的な違いです。
スクリプト呼び出しの仕組み
スクリプト関数が別の関数を呼び出す(再帰呼び出しを含む)とき、VMは:
- フレーム情報をVMスタックにプッシュ - Cスタックではない
- プログラムカウンタを更新 して対象関数にジャンプ
- ディスパッチループを継続 - 別のC関数を呼び出すことはない
これにより:
- スクリプトで
fib(1000)を実行してもVMスタックメモリを消費するだけでCスタックは消費しない - C言語のスタック深度はスクリプトの再帰深度に関係なく一定
- ホストMCUでの
HardFaultやスタックオーバーフローのリスクがない
従来のインタープリタとの対比
ナイーブなインタープリタ(危険):
- スクリプト関数呼び出し → C関数呼び出し
- 深い再帰 → Cスタックオーバーフロー
- 予測不能なメモリ消費
Lambda C(安全):
- スクリプト関数呼び出し → VMスタック操作 + ジャンプ
- 深い再帰 → VMスタック消費のみ
STACKSIZE内での決定論的なメモリ消費
商用上の意義
このアーキテクチャにより実現されること:
- RTOS互換性 - 最小限のCスタック割り当て(例: 1-2KB)でVMをタスク内で実行可能
- 予測可能な最悪ケースメモリ - メモリ消費 =
vm_stack_size + vm_garea_size + vm_heap_size - 隠れたスタック消費なし - C関数を再帰的に呼び出すインタープリタとは異なる
ロード時リンク: O(1) FFIディスパッチ
実行時シンボル解決の問題
従来のスクリプト言語(Lua、Python)は、関数名を呼び出すたびに解決します:
- 文字列ハッシュ計算
- ハッシュテーブルルックアップ
- 長いシンボル名でのキャッシュミス
これにより可変で予測不能なFFI呼び出しオーバーヘッドが発生します。
Lambda Cの解決策
FFIリンクはバイトコードロード時に一度だけ行われます:
- 登録フェーズ: ホストが文字列名で関数を登録
- リンクフェーズ: バイトコードローダーがすべての関数名参照を整数IDに変換
- 実行フェーズ: FFI呼び出しは整数ID → 直接テーブルルックアップを使用
結果: 以下に関係なくO(1)の定数時間FFIディスパッチ(約100ns):
- 関数名の長さ
- 登録された関数の数
- 呼び出し頻度
なぜこれが重要か
100Hz-10kHzで実行される制御ループにおいて:
- 予測可能なタイミング - 文字列ルックアップからの可変オーバーヘッドなし
- リアルタイム対応 - 一貫したFFI呼び出しレイテンシ
- キャッシュフレンドリー - 整数テーブルルックアップ vs 文字列ハッシング
型スタック: なぜランタイム型追跡が必要か?
Compactバイトコードのトレードオフ
Lambda Cの4バイト命令フォーマットは、命令に型情報をエンコードしないことで極端なコンパクト性を実現しています。ADD命令はintを加算しているのかdoubleを加算しているのかを指定しません。
型スタックによる解決
VMは、VMスタック上の各値のランタイム型を追跡する並列型スタックを保持します:
int型は高速整数専用パスを有効化double型は浮動小数点演算をトリガーpointer型はサンドボックス制限を強制
重要な洞察: これは組み込み向けに最適化された空間-時間トレードオフです:
- 節約される空間: 型タグ付き8バイト命令ではなく4バイト命令
- 時間コスト: 型スタックルックアップ(命令フェッチと比較して最小限)
性能最適化: 高速整数モード
VMが整数のみの演算を検出すると、以下が可能になります:
- 型タグチェックをスキップ
- CPU整数ALUを直接使用
- 不要な浮動小数点変換を回避
これが、インタープリタであるにもかかわらずfib(25)が高速に実行される理由です - ホットループがすべて整数演算だからです。
静的型モード (-Os)
コンパイラの -Os オプションは、型情報を完全に省略したバイトコードを生成します:
lcvmc -Os -oc output.lcbc source.c
技術的効果:
- スタックエントリあたり12バイトの型情報を省略
- 型チェックのオーバーヘッドを完全に排除
- 約2倍の実行速度向上(fib(25): 27ms → 13ms)
トレードオフ:
- コンパイル時に型が確定する整数演算プログラムに限定
- printf/sprintf対応済み。組み込み向けに
LCVM_PRINTF_BUFSIZEでバッファサイズ設定可能(デフォルト: 1024バイト)
これは空間と時間の両方を最適化する選択肢であり、組み込みシステムの厳しい制約に適しています。
Arena Allocator: 意図的にfree()なし
なぜfree()がないのか?
従来のmalloc/freeは以下を引き起こします:
- ヒープフラグメンテーション - 繰り返しalloc/freeサイクル後のメモリホール
- 非決定論的な割り当て時間 - フリーリスト走査オーバーヘッド
- 予測不能な失敗 - 十分な合計空きメモリがあっても割り当てが失敗する可能性
Arena Allocatorの特性
Lambda Cのアロケータは:
- 連続バッファから順次割り当て
- 個別割り当ての解放は決してしない - 一括リセットのみ
- O(1)割り当て時間 - バンプポインタ、フリーリスト検索なし
- フラグメンテーションゼロ - メモリホールなし
組み込みユースケースへの適合
この設計は定期的な制御ループに最適です:
制御ループの反復:
1. ヒープリセット → 新鮮なメモリで開始
2. 一時バッファ割り当て → O(1)高速
3. センサーデータ処理 → フラグメンテーションリスクなし
4. 制御信号出力 → 予測可能なメモリ
5. 次の反復 → ヒープリセット、繰り返し
トレードオフ: 定期的にlcvm_heap_reset()を呼び出す必要があります。これは許容可能です。なぜなら:
- 組み込み制御ループは自然に反復境界を持つ
- リセットコストはループ周期(例: 10ms)と比較して無視できる
- すべてのフラグメンテーションとリークリスクを排除
Heap Watermark API
Arena Allocatorの制限を補完する機能として、Heap Watermark APIを提供:
size_t mark = lcvm_heap_mark(); // 現在位置を記録
char *buf = lcvm_malloc(64*1024); // 一時確保
// ... 処理 ...
lcvm_heap_release(mark); // マーク位置まで一括解放
特性:
- O(1)解放時間 - 単純なポインタ移動
- フラグメンテーションなし - Arena設計を維持
- 部分解放対応 - 全体リセットなしで一時メモリを解放
これにより、長時間稼働システムでのメモリ効率が大幅に向上します。
4バイト命令: キャッシュ効率
なぜ固定長が重要か
可変長命令セット(x86、Luaバイトコード):
- 予測不能な命令フェッチ時間
- 貧弱なキャッシュ利用
- 複雑な分岐予測
固定4バイト命令:
- 命令ごとに1回のメモリフェッチ - 常に4バイト
- キャッシュラインフレンドリー - 32バイトキャッシュライン(典型的なARM Cortex-M)あたり8命令
- 予測可能なループタイミング - 可変長デコードオーバーヘッドなし
コンパクトかつ完全
4バイトで提供されるもの:
- 8ビットオペコード(256命令タイプ)
- オペランド用24ビット(3×8ビットまたは組み合わせアドレッシングモード)
これで十分なもの:
- すべての算術演算
- メモリアクセスパターン
- 制御フロー(ジャンプ、呼び出し、リターン)
- FFIディスパッチ
設計原則: ROM/RAMが希少でCPUサイクルが十分な組み込みシステムでは、命令密度 > 生デコード速度。
決定論的メモリ消費
セーフティクリティカル要件
商用組み込みシステムでは、最悪ケースメモリ消費を証明する必要があります。Lambda Cはこれを可能にします:
合計メモリ = vm_stack_size + vm_garea_size + vm_heap_size
境界の証明
- VMスタック:
STACKSIZEで決定される最大深度 - スタックオーバーフロー検出がこれを超えないようにする - グローバル領域: バイトコードのグローバル変数宣言に基づきロード時に固定
- ヒープ:
heap_sizeで境界付けられたArena allocator - 使い果たすと割り当ては明確に失敗
隠れた割り当てなし:
- GCヒープ成長なし
- 一時バッファを割り当てる暗黙的変換なし
- コードを生成する実行時コンパイルなし
認証上の利点
この決定論性は以下に価値があります:
- 医療機器 - FDAは最悪ケースメモリ分析を要求
- 自動車 - ISO 26262 ASIL安全レベル
- 産業制御 - IEC 61508 SIL要件
比較: Lambda CがLua/Pythonと異なる理由
アーキテクチャ哲学
| 側面 | Lambda C | Lua | MicroPython |
|---|---|---|---|
| Cスタック使用 | 定数 | 呼び出し深度で増加 | 呼び出し深度で増加 |
| FFIディスパッチ | O(1)テーブルルックアップ | O(1)ハッシュテーブル(キャッシュ後) | 文字列比較 |
| メモリ割り当て | Arena(freeなし) | マーク・スイープGC | マーク・スイープGC |
| 命令フォーマット | 4バイト固定 | 可変(1-4バイト) | 可変(1-3バイト) |
| 型追跡 | 型スタック | 値ごとのタグ | PyObjectヘッダ |
組み込み向け設計
Lambda Cは決定論性と安全性に最適化されたトレードオフを行います:
- 柔軟性より予測可能性
- 動的より境界付き
- 表現力よりシンプル
これが、Lua/Pythonが適さないセーフティクリティカル組み込みシステムに適している理由です。
まとめ: 技術評価
Sグレード: アーキテクチャ
非再帰VMループ: スクリプト呼び出しスタックとCスタックの完全分離。最小限のスタック割り当てでRTOSタスクに安全。
ロード時リンク: O(1) FFIディスパッチが実行時シンボル解決オーバーヘッドを排除。リアルタイム制御アプリケーションを可能にする。
Sグレード: メモリ効率
4バイト命令フォーマット: キャッシュフレンドリーな固定長設計を維持しながら極端なROM効率を達成。
Arena allocator: フラグメンテーションゼロ、O(1)割り当て、決定論的メモリ消費。定期的制御ループに理想的。
Sグレード: 商用適合性
決定論的メモリ境界: コンパイル時に合計メモリ消費が証明可能。安全認証を可能にする。
AOTコンパイル: パース複雑性を開発環境に分離。ランタイムはシンプル、境界付き、検証可能。