JITの穏やかな紹介と動的コンパイル/コード生成
-
04-07-2019 - |
質問
C / C ++フレームワーク内での動的コード生成の見かけ上単純な基盤については、既に別の質問で取り上げています。コード例でトピックに優しい紹介がありますか?
私のニーズが控えめなとき、私の目は非常に複雑なオープンソースJITコンパイラーを見つめ始めています。
コンピュータサイエンスの博士号を前提としない主題に関する良いテキストはありますか?使い古したパターン、注意すべき点、パフォーマンスの考慮事項などを探しています。電子またはツリーベースのリソースも同様に価値があります。 (x86だけでなく)アセンブリ言語の実用的な知識を想定できます。
解決
まあ、エミュレータで使用したパターンは次のようになります:
typedef void (*code_ptr)();
unsigned long instruction_pointer = entry_point;
std::map<unsigned long, code_ptr> code_map;
void execute_block() {
code_ptr f;
std::map<unsigned long, void *>::iterator it = code_map.find(instruction_pointer);
if(it != code_map.end()) {
f = it->second
} else {
f = generate_code_block();
code_map[instruction_pointer] = f;
}
f();
instruction_pointer = update_instruction_pointer();
}
void execute() {
while(true) {
execute_block();
}
}
これは単純化ですが、アイデアはそこにあります。基本的に、エンジンが「基本ブロック」の実行を要求されるたびに、 (通常、次のフロー制御操作または機能全体までのすべて)、それが既に作成されているかどうかを確認するために検索されます。ある場合は実行し、そうでない場合は作成し、追加してから実行します。
リンスリピート:)
コード生成に関しては、それは少し複雑になりますが、アイデアは適切な「関数」を出すことです。これは、VMのコンテキストで基本ブロックの作業を行います。
編集:最適化のデモも行っていないことに注意してください。ただし、「穏やかな紹介」
編集2:このパターンで実装できる最もすぐに生産的なスピードアップの1つに言及するのを忘れました。基本的に、ツリーからブロックを削除しない場合(実行しない場合は回避できますが、実行しない場合ははるかに簡単です)、「チェーン」できます。ルックアップを避けるために一緒にブロックします。これがコンセプトです。 f()から戻って&quot; update_instruction_pointer&quot;を実行しようとするたびに、実行したばかりのブロックが呼び出し、無条件ジャンプ、またはフロー制御で終了しなかった場合、「修正」することができます;実行する次のブロックへの直接jmpを使用したret命令(常に同じであるため) 既にそれを発行している場合。これにより、VMでの実行回数が増え、「execute_block」での実行回数が減ります。関数。
他のヒント
特にJITに関連するソースは認識していませんが、通常のコンパイラとほとんど同じで、パフォーマンスが心配されない場合にのみ簡単になると思います。
最も簡単な方法は、VMインタープリターから始めることです。次に、各VM命令について、インタープリターが実行するアセンブリコードを生成します。
それを超えるには、VMバイトコードを解析して、適切な中間形式(3アドレスコード?SSA?)に変換し、他のコンパイラーと同様にコードを最適化して生成すると思います。
スタックベースのVMの場合、「現在」を追跡すると役立つ場合があります。バイトコードを中間形式に変換し、各スタック位置を変数として扱う際のスタック深度。たとえば、現在のスタックの深さが4であると思われる場合、「プッシュ」が表示されます。命令、&quot; stack_variable_5&quot;への割り当てを生成できます。コンパイル時スタックカウンターなどをインクリメントします。 「追加」スタックの深さが5の場合、コード&quot; stack_variable_4 = stack_variable_4 + stack_variable_5&quot;が生成される場合があります。コンパイル時スタックカウンターをデクリメントします。
スタックベースのコードを構文ツリーに変換することもできます。コンパイル時スタックを維持します。すべての「プッシュ」命令により、プッシュされるものの表現がスタックに格納されます。演算子は、オペランドを含む構文ツリーノードを作成します。たとえば、&quot; X Y +&quot;スタックに&quot; var(X)&quot;が含まれ、その後&quot; var(X)var(Y)&quot;が含まれる場合があります。そして、プラス記号は両方のvar参照をポップし、&quot; plus(var(X)、var(Y))&quot;をプッシュします。
Joel PobarのRotorの本のコピーを入手して(外に出たら)、ソースを調べて SSCLI 。注意してください、狂気は内にあります:)