SBCL: アセンブリコードのブレッドボード

仮想マシン (VM) 設計と低レベルマシンコードの最適化の交差点では、柔軟性とパフォーマンスの間の緊張関係がしばしば明らかになります。従来のスタックベースの VM では、データのプッシュやポップに伴うオーバーヘッド(メモリへのアクセスや複雑なレジスタの入れ替えを伴うことが多い)が、重大なボトルネックとなる可能性があります。

この技術的な探求では、Steel Bank Common Lisp (SBCL) アセンブラを活用して、高性能なスタックベース VM を実装する方法を検討します。目標は、少数のハードウェアレジスタをローテーションするスタックとして扱うことで、スタックポインタのあらゆる可能な位置に対してマシンコードを特化させ、スタック操作中のデータ移動を排除することです。

ローテーション・スタックの概念

F18 や x87 浮動小数点演算ユニットに触発された核心となるアイデアは、VM スタックを少数の固定されたスロット(例:8)に制限することです。pushpop の際にレジスタ間やメモリ間で物理的にデータを移動させる代わりに、VM は現在のスタックトップ (TOS) を指すモジュラーカウンタを維持します。

ソフトウェア実装において、レジスタを動的にインデックス化することは、通常不可能であるか、あるいは極めて低速です。これを解決するために、VM は primitive specialization(プリミティブの特化)と呼ばれる手法を採用しています。すべてのプリミティブ操作(ADDDUP など)に対して、VM はスタックカウンタの可能なすべての値に対して、8 つの異なるバージョンのマシンコードを生成します。

レジスタ・マッピング

これを x86_64 で実装するために、以下のレジスタ割り当てが使用されます:

  • r8 から r15: 8 つのスタックスロット。
  • rsi: マシンコード・プリミティブのベースアドレス。
  • rdi: 仮想命令ポインタ (VIP)。
  • rax, rbx, rcx, rdx: スクラッチレジスタ。
  • rsp: 仮想リターン・スタックポインタ。

SBCL による VM の実装

このプロジェクトにおいて、SBCL は「マクロアセンブラ」として機能します。SBCL はマシンコードの対話的な生成と検査を可能にするため、仮想スタックポインタの現在の状態に基づいてアセンブリを放出する Lisp 関数を書くことが可能です。

NEXT シーケンス

ダイレクトスレッド型 VM では、各プリミティブは次の命令へジャンプするための NEXT シーケンスで終わります。ローテーション・スタックをサポートするために、NEXT シーケンスは、新しいスタックポインタの値に一致する次のプリミティブのバージョンへとディスパッチする必要があります。

実装では、各 primop のバリアントを一定の間隔(例:4288 バイト間隔)で格納します。 variant_offset = 4288 * stack_counter

これにより、VM は実行時に条件チェックや複雑なルックアップテーブルを実行することなく、現在のスタックの深さに特化したコードへ直接ジャンプすることができます。

命令セットの構築

基本プリミティブ

SWAP, DUP, ADD のような単純な操作は、現在 TOS とその下の要素にマッピングされているレジスタに対して、特定の x86 命令を放出することで実装されます。例えば、ADD 操作は、単に (@ 1) にあるレジスタと (@ 0) にあるレジスタを加算し、仮想スタックポインタをインクリメントします。

制御フロー

VM を機能させるためには、制御フロー・プリミティブが不可欠です:

  • JMP: 仮想 IP を相対オフセットで上書きします。
  • CALL/RET: ハードウェア・スタック (rsp) を使用して、仮想 IP のリターンアドレスを保存します。
  • Conditionals (JZ/JNZ): CMOV (条件付き移動) または複製された NEXT シーケンスを使用して、仮想 IP を TOS の値に基づいて更新します。

パフォーマンス分析と最適化

この実装における最も重要な発見の一つは、オペレーターの「融合」が与える影響です。カウンタを減算し、ゼロでなければジャンプする単純なループは、いくつかの方法で実装可能です:

  1. Unspecialized Bytecode: LIT $\rightarrow$ SUB $\rightarrow$ JNZ. これは最も低速なアプローチです(ネイティブコードより 11-15 倍低速)。
  2. Fused Operator (DJN): 減算とジャンプを同時に扱う単一のプリミティブ。これによりパフォーマンスが大幅に改善されます。
  3. Optimized Fused Operator (DJN2): 条件付き移動 (CMOV) を、予測可能なハードウェア分岐に置き換え、NEXT シーケンスを複製することで、ループはネイティブ・アセンブリの約 6 倍低速になります。

比較要約

実装 相対的パフォーマンス (Cycles/Iter)
Native Assembly 1x
Specialized DJN2 ~6x
Specialized DJN ~8x
Unspecialized Bytecode 11-15x

結論

スタックサイズが小さく保たれる限り、スタックポインタに基づくプリミティブの特化は、スレッド型インタプリタのオーバーヘッドを削減するための実行可能な戦略です。著者は、主にスタック言語に関心があるわけではないと述べていますが、このアーキテクチャは効率的なランタイム中間表現 (IR) として機能します。

このプロジェクトは、低レベル・システム・プログラミングのツールとしての SBCL の力を強調しています。高レベル Lisp 抽象化と生のマシンコード生成の間の架け橋を提供することで、SBCL は、C や従来のアセンブリ言語では煩雑になるであろうアセンブリ技術の「ブレッドボード」的な迅速な試行錯誤を可能にします。

Sources