gotoが無いなら、バイトコードを書き換えればいいじゃない
ってマリーアントワネットも言ってました。
(えー)
blogで前述のとおり、Javaにはgotoがありませんが、Java bytecodeを含む、機械語というものはgotoのような直接ジャンプ命令があるのが普通です。
そのため、トランスレータによって機械語を高級言語のソースに変換する場合は、ジャンプ命令をgotoに対応付けて変換を行ったほうが簡単だし、性能が出ます。
そこで、Javaコンパイラが生成したバイトコードを、直接的に書き換えてgoto命令を埋め込むことできないか調べることにしました。
今回、gotoに置き換えたいのは、以下のようなパターンです。
while (clk > 0) { switch (pc) { case 0xC801: clk -= 6; PushStackW(0xC801 + 2); pc = 0xCBAE; continue; /* Absolute JSR 0xCBAE */ case 0xC804: clk -= 3; pc = 0xCBB7; continue; /* Absolute JMP 0xCBB7 */ case 0xC807: clk -= 2; x = a; /* Implied TAX */ case 0xC808: clk -= 4; a = LdMemB(50343 + x); /* AbsoluteX LDA 50343+x */ case 0xC80B: clk -= 3; wram[0] = a; /* ZeroPage STA 0 */ case 0xC80D: clk -= 4; t = a = LdMemB(50344 + x); /* AbsoluteX LDA 50344+x */ case 0xC810: clk -= 3; wram[1] = a; /* ZeroPage STA 1 */ case 0xC812: clk -= 3; pc = 0xF228; continue; /* Absolute JMP 0xF228 */ case 0xC815: clk -= 2; x = a; /* Implied TAX */ case 0xC816: clk -= 4; a = LdMemB(49212 + x); /* AbsoluteX LDA 49212+x */ case 0xC819: clk -= 3; wram[2] = a; /* ZeroPage STA 2 */ /*(以下略)*/
6502のメモリアドレスを、Java命令列に対応付け、ジャンプ命令を処理するために、switch文を使っています。
ここで、Absolute JSRや、Absolute JMPのような、pcに定数を入れて、continueを使ってジャンプするパターンに注目します。
case 0xC804: clk -= 3; pc = 0xCBB7; continue; /* Absolute JMP 0xCBB7 */
は、
case 0xC804: clk -= 3; pc = 0xCBB7; goto case 0xCBB7; /* Absolute JMP 0xCBB7 */
と書くことができれば、より高性能になるはずです
(Javaではこのような記述はできませんが、C#では可能です)。
このようなpcに定数を入れてcontinueを実行するパターンが、どのようなバイトコードにコンパイルされるかを調べて、それに対してパッチを当てるような感じで書き換えを行えばよさそうです。
switch命令は、tableswitchにコンパイルされる前提とします。
tableswitchのOPCODEは0xAAです。
その後、4byte境界にアラインするためのパディングが入って、
「defaultのjump offset」4byte、「caseの最小値」4byte、「caseの個数-1」4byteと、Operandが並びます。
そこから4byteづつ、jump先へのoffsetが並ぶ構造になってます。
jump先へのoffsetは、0xAAのOpecodeがあるアドレスからの相対値です。
classファイルを配列に読み込んで、先頭から0xAAを探します。
簡単化のため、classファイルのヘッダなどを全部無視します。
当然、tableswitchのopecodeではない0xAAというデータがたくさんでてくるはずですが、0xAAに続いて「caseの最小値」「caseの個数-1」が一致するかどうかを調べることでそれがtableswitchかどうかの照合を行います。
この処理で、classファイル中のtableswitchによるジャンプテーブルの位置を探し出し、ジャンプテーブルの値を読み出していき、6502のメモリアドレス(case句に指定した値)と、ジャンプテーブルのoffset値を、ペアにして配列に保持していきます。
これをPCTableと呼ぶことにします。
できあがったPCTableを使用して、pcに定数を代入してcontinueを実行しているcase句の、一個次のcase句の場所を探します。ここから3byte戻ったところに、goto命令があるはずです。gotoは0xA7というオペコードです。
これはcontinue命令がコンパイルされた結果のgoto命令なので、switchの頭にジャンプしているはずです。
このジャンプ先を、もう一度PCTableを引いた値で書き換えることによって、switchの頭ではなく、pcに代入した値のcase句のあるアドレスへ直接ジャンプするようにします。
なんていうか、文章にすると、超わけがわからない感じなんですが、バイナリエディタでclassファイルを見ながら手でやってみると大した処理ではありません。
こんな感じで、javacが生成したclassファイルを書き換えることにより、高速化を行うことができました。
性能向上の評価は、また後でやります。