Virtual Machines: Versatile Platforms For Systems And Processes (The Morgan Kaufmann Series in Computer Architecture and Design)Virtual Machines: Versatile Platforms For Systems And Processes

やっとダイナミックなバイナリトランスレートの話がでてきましたよ。

(2009.2.9 Chapter毎に整理しました)

Source ISAとTarget ISAが異なるときの話。しかし、そうじゃない場合にもこのテクニックは使える。binary optimizationとか、特権命令実行時のemulation的動作とか。

2.1 Basic Interpretation

普通のエミュレーション。メインループがあって、デコード・命令ディスパッチを実行。ブランチが多すぎて遅い。

この章で突然「FORTHは"threaded code" interpretation modelで有名」と出てきて驚く。FORTHは昔だいぶ遊んだけど、それ知らないぞ(と思ったのですが、threaded codeのところを読んだら納得できました)

2.2 Threaded Interpretation

各Instruction emulationの最後に、decodeして対応するインストラクションを実行する。テーブルをひき、そこへ飛ぶコードを追加する。メインループはもういらない。これは、デコード結果からテーブルをひいてジャンプ、なので
"indirect" threaded interpretationと呼ばれる。

2.3 Predecoding and Direct Threaded Interpretation

ループは消えたけど、毎回デコードのオーバーヘッドがある。

2.3.1

そこでプレデコード。デコードして扱いやすい形で再度格納。

2.3.2 Direct Threaded Interpretation

デコード結果の「オペランド」のところに、そのオペランドを実行するアドレスを直接いれる。メモリルックアップが1回減らせる。

2.4 Interpreting a Complex Instruction set

ここまではPowerPCを例にしていたが、IA32みたいなCISCでは大変。

IA32だと、General Decodeしてから、特殊な処理に分岐するのが基本的な動き。

2.4.2 Threaded Interpretation

RISCに比べてdecode処理が複雑ででかい。各instruction interpreterの最後にdecode処理を書いていたら、インタプリタがでかくなる。

ということで、一般的なケースに特化したdecode-and-dispatchルーチンをいれる。特殊なケースはブランチしてとばす。

Direct Threaded Interpretationをやろうとするといろいろ問題。とくに2つ:

  1. predecoded instructionは長くなる傾向がある。メモリをいっぱいくう。
    instruction types毎に中間コード用意するなど
    あるいは、小さいインストラクションに分解するなど → これはCISCからRISCへのbinary translateに似ている
  2. code discovery。実行しないと命令の境界が分からない場合があり、プレデコード困難。
    binary translationでも問題になるので、あとでくわしく取り扱う。

また、2段階のdecode-dispatchになる。これもbinary translationと非常に似ている。
binary translationはポータビリティに欠ける、が、retargetable binary translatorが開発されている
(p46)

2.4.3 High-performance IA-32 Interpreter

FX!32をベースにした例。オリジナルのAlphaじゃなくて、PowerPCのアセンブラで記述されているが、PowerPCをよく知らないのでほとんど理解できない。IA32の1インストラクションをAlphaの30インストラクション相当で実行できるらしい。

2.5 Binary Translation

2.4まではインタプリタだけど、target ISAのバイナリコードに変換するとパフォーマンスがあがる。

インタプリタと違って、source ISA状態のマッピングをtarget ISAのレジスタに直接おくことで、コンテクストブロックをメモリから読み込むオーバーヘッドを削除できる。

2.6 Code Discovery And Dynamic Translation

2.6.1 The Code Discovery Problem

スタティックにプレデコードしたりバイナリトランスレートしたりすることを考えてみる。(特にCISCなISAでは)これはかならずしも簡単じゃない(それどころか不可能な場合もある)。命令境界がどこかわからないし、生成されたコードのなかにデータとかパディングがはいっていることだってある。

RISCであれば、1命令1ワードだったりするが、たとえばIA32だと、任意のバイトが本当に命令境界なのか判断する方法はない。

2.6.2 The Code-Location Problem

間接ジャンプだと、ターゲット側に展開された命令列のどこに飛べばいいかわからない(ただしRISCやJavaVMではこういう問題は起こらない)

2.6.3 Incremental Predecoding and Translation

2.6節のハイライト! (ってそんなに気合いをいれるほどのことはない)。プレデコードとトランスレーションはきわめて似たプロセスなので、この本では「トランスレーション」と呼びます。

Emulation Manager、インタプリタとトランスレータ、トランスレートされたコード(のキャッシュ)、そしてソースPCからターゲットPCへのマッピングを示すハッシュテーブルをそろえれば、ダイナミックにインクリメンタルなトランスレートができます。

Emulation Managerは、マップを使ってコードキャッシュを探し、ヒットすればトランスレートされたブロックを呼びます。ヒットしなければ、インタプリタ・トランスレータを呼んで、次のブロックをインタープリト・トランスレートし、マップにトランスレートされた結果をいれます。

このときの、トランスレートの単位は「Dynamic Basic Blocks」。ブランチで入ってきたポイントから、ブランチで出るポイントまでを(動的に)認識したものです(これにたいして、Static Basic Blocksは、静的構造を解析してエントリポイント・エグジットポイントで切ったもの)。

Dynamic Basic Blocksの特徴として「他の大きなブロックの一部が、新たに小さなブロックになることがある」です。これは、大きなブロックの途中に、エントリポイントがある場合などですね。(p57の図がわかりやすい) このダブりがあることで、すでにトランスレートされたコードの途中にブランチしたら、みたいな複雑な処理がいらなくなっています

こんどは、ブロックごとに「キャッシュを探す」「Emulation Managerに戻る」オーバーヘッドをへらすことが考えられます。キャッシュを探す、については、SourceのPCをTargetのレジスタにいれとくとか、コードブロックの最後に次のSource PCを置いておき、そこをlink register経由でアクセスする方法とかがあります。

他にも問題がいろいろ。これらはChapter3や4で扱われます。

  • self-modifying code
  • self-referencing code
  • precise traps
2.6.4: Same-ISA Emulation

同じISAでもEmulationがありうる。Emulation Managerが常にコントロールをもち、かつモニタ(= code management)できる。この一例が"simulation"。2.9でcase studyとして扱われる。

2つめの例は、ISAが同じだがOSが異なる場合。このときはsystem callをemulationする。

3つめの例は、特権命令を扱うエミュレーション(Chapter 8)。

4つめは、Chapter10であつかう"program shepherding"。セキュリティ的監視。

さいごが、ランタイムでのbinary optimization。

Chapter 3に突入したけど、ここでの復習が追いつかない!

2.7 Control Transfer Optimization

2.6までの「シンプルな」トランスレート方法では、Source PC/Target PCのルックアップや、ブロックごとにEmulation Managerへコントロールが戻ることがどうしても起きる。このオーバーヘッドを減らす方法いろいろ。

2.7.1 Translation Chaining

インタプリンタのスレッディングと同じようなテクニック(名前から見当つくよね)。トランスレートされたブロックをチェーンするのだ。

次のブロックがまだトランスレートされていない場合は、通常のスタブコードが入る。

2.7.2 Software Indirect Jump Prediction

ソフトウェア分岐予測! これは本質的には「inline caching」の実装である。分岐先のコードを、出現しそうな順に、ifのなかで即値で埋め込んでおくのです。

なので、profilingとセットでやる必要がある。

2.7.3 Shadow Stack

sourceのstackへ戻り値を格納すると同時に、target側でEmulation Managerが管理する"shadow stack"にtargetの戻り先PCを格納する。stackからpopするときは値のチェックをしてconsistencyをみる。

VM本2章の終わりのほうです。

2.8 Instruction Set Issues

インストラクションセットエミュレーションについて、まだまだいろいろあるけどそのうちの一部を扱います。

2.8.1 Register Architectures

レジスタはストレージヒエラルキのてっぺんにいるので、パフォーマンスに影響がでかい。

レジスタ割り当ての戦略についてbinary translation, interpreterのそれぞれで、一般論を。

2.8.2 Condition Codes

フラグの類について。sourceとtargetでISAが違えば、同じではない。MIPSみたいにcondition codeがないようなものもある。

source instructionの実行のたびにcondition codeを更新する手もあるが、これは遅いし、通常必要でもない。lazy evaluationがよく使われるテクニック。condition codeに影響がある、最近実行された命令とそのオペランドを記録しておき、必要になったらcondition codeを生成する。

この方法でも、実行中にtrapが発生した場合に問題がある。trap発生したら、preciseなソース状態を作らなくてはならない。このへんはあと(4.5.2)で議論する。

2.8.3 Data Formats and Arithmetic

浮動小数点のフォーマットはかなり統一されているが、たとえばIA32では80bitの中間結果がつかわれるが、ほかの多くのISAでは64bitみたいな差がある。

ほかにもいろいろ。PowerPCでは連続した乗算・加算のインストラクションがあるが、これはエミュレートできない場合がある(途中でオーバーフローしちゃうとか)。アドレッシングモードが違うとか、即値の長さが違うとか。

2.8.4 Memory Address Resolution

sourceとTargetでアクセスできるメモリの単位が違う。たとえばbyte単位でのメモリアクセスができないISAもある。

2.8.5Memory Data Alignment

メモリのアラインメントもISAで違う。いろんなやりかたがあるけど、プロファイルをとるような方法もある。Chapter4で議論する。

2.8.6 Byte Order

BigEndian, LittleEndianなISAがある。

両方サポートしているISAもある。

2.8.7 Addressing Architecture

サイズが違う、特権レベルが違うなどいろいろ。

2.9 Case Study: Shade and the Role of Emulation During Simulation

"Shade"という、high-performanceなsimulationのケーススタディ。

"simulation"は、「内部動作までのシミュレート」を意味する。emulationは表面の動作のみ。

あまり細かいことはかいていないが、code cacheのメカニズムがシンプル。source PCからhash経由で、translated codeの場所がひかれる。hashのさすさきには、n個の"source/target"ペアがはいってる。新しく使われたのが、せんとうにくる。もしいっぱいになれば、n個目が押し出される。そういうわけで、translateされたけど、danglingになるtranslated codeがある(本文中では"orphan"って表現されてる)。

code cacheはあたまからつめられていく。もし最後に到達したら、単純にすべてフラッシュされる。そういうわけで、danglingなコードはそのうち消える。

このメカニズムはシンプルだけど、効率ほんとにいいのか? と疑問に思うが、いいんだろうな、確かめるためには論文にあたればよさそう。

2.10 Summary: Performance Tradeoffs

2章のまとめ。つぎのものたちが出てきています。

Decode-and-Dispatch
メモリ要求少ない、スタートアップ速い、定常動作遅い、ポータビリティ高い
Indirect Threaded Interpreter
メモリ要求少ない、スタートアップ速い、定常動作遅い(Decode-Dispatchよりはまし)、ポータビリティ高い
Direct Threaded Interpreter with Predecoding
メモリ要求多い、スタートアップ遅い、定常動作普通、ポータビリティ普通
Binary Translation
メモリ要求多い、スタートアップとても遅い、定常動作速い、ポータビリティダメ

Virtual Machine本ひとり勉強会