x86エミュレータチームがあまりにひどいコードを見つけ、エミュレーション中に修正してしまった話
(devblogs.microsoft.com)- x86-32エミュレータは、別のプロセッサでx86-32コードを実行するためにバイナリ変換でネイティブコードを生成し、インタプリタ方式より大きな性能向上を提供していた
- このエミュレータはx86-32をバイトコードのように見なし、エミュレータをJITコンパイラのように動作させる構造として理解できる
- あるプログラムはスタック上に約64KBのメモリを割り当てて初期化する必要があり、一般的な方法はスタックプローブ後にスタックポインタを減らし、小さなループでメモリを初期化するものだった
- そのコードのコンパイラはループの代わりに65,536個の個別のバイト書き込み命令を生成し、各命令が4バイトだったため、64KBのデータを初期化するのに256KBのコードが必要だった
- エミュレータチームはこの関数を検出する特殊なコードをトランスレータに追加し、等価な短いループに置き換えるようにした
背景: x86-32エミュレータとバイナリ変換
- Windowsには、x86-32以外のプロセッサで動作するシステム向けに、x86-32プロセッサエミュレータが含まれていたことがあった
- この事例がどのプロセッサに適用されたかは、原文では特定されていない
- このエミュレータはバイナリ変換を使って、元のx86-32コードと等価な動作を行うネイティブコードを生成していた
- この方式はインタプリタベースのエミュレーションより大幅な性能向上をもたらした
- x86-32をバイトコードとして見なし、エミュレータをJITコンパイラとして捉えるような理解が可能である
問題のコード: 64KBのスタックメモリ初期化
- あるプログラムはスタック上に約64KBのメモリを割り当て、これを初期化する必要があった
- 標準的な方法は、まずスタックプローブを実行して64KBのメモリを使用できるか確認する手順だった
- その後、スタックポインタから65,536を引き、小さくタイトなループでメモリを初期化するのが一般的だった
コンパイラの過剰なループアンローリング
- そのコードをコンパイルしたコンパイラは、各バイトを初期化するループを生成しなかった
- 代わりに、ループを65,536個の個別の**「メモリへのバイト書き込み」命令**として展開して生成した
- 各命令の長さは4バイトだった
- 結果として、64KBのデータを初期化するために256KBのコードが必要になった
エミュレータチームの対応
- エミュレータチームはこの関数を検出する特殊なコードをトランスレータに追加した
- 検出された関数は、等価な動作を行う短いループに置き換えられた
- この処理は、元のプログラムコードをそのまま翻訳する代わりに、エミュレーション中に非効率なコードパターンをより簡潔な形に変える方式だった
1件のコメント
Lobste.rsの意見
Raymond Chenにループ展開(loop unrolling) を説明していたコメントがかなり気に入った
こうした記事を読む人の中には、背景知識をすべて知っているわけではないので、さらに学べる手がかりをありがたく思う人も多い
https://joelonsoftware.com/2004/06/…
これはAlphaでのことだった気がする。そのプラットフォーム向けのx86エミュレータには非常に多くの作業が投入されていたからだ