自作Binary Translation型RISC-Vエミュレータで性能向上を図っているが、どうにも上手く行かない。いろんな方法を試行しているのだが、いくつかの試行についてメモをしておこうと思う。
即値生成をEAX以外でもできるようにする
即値生成はEAXに対してのみできるのかと思っていたが、実はそうでもなかった。32ビットまでの値であればEAX以外のレジスタに対しても即値を生成できる。これまでは32ビットの即値生成に対して、
MOV $imm, rax MOV rax, target_x86reg
という2段階で生成していたが、これは実は1命令に省略できるということを知った。まあこれも考えてみれば当たり前の話ではあるが、どういう事情でRAXにレジスタを制限してしまったのかは忘れてしまった。たぶん誤った資料を見たか、勘違いした。
冗長なレジスタ読み込みと書き込みを省略
例えば以下の加算命令は、TCGでは4つのステップに分けられる。
ADD x10, x11, x12
- x11の値をx86の任意のレジスタにロードする。(
MOV 0x88(RBP), RDX
) - x12の値をx86の任意のレジスタにロードする。(
MOV 0x90(RBP), RBX
) - 加算をする (
ADD RDX, RBX
) - x10を結果をストアする (
MOV RDX, 0x80(RBP)
)
MOV 0x88(RBP), RDX MOV 0x90(RBP), RBX ADD RBX, RDX MOV RDX, 0x80(RBP)
まあこの命令群はもう少し省略できるとかいう話は置いておいて。
MOV 0x88(RBP), RDX ADD 0x90(RBP), RDX MOV RDX, 0x80(RBP)
さらにもう1命令繋げてみる。
ADD x10, x11, x12 ADD x9, x10, x8
同じTCGから1命令ずつ生成するので、単純に命令を連続して生成すると以下のようになる。
# 最初のADD命令で生成されるx86命令 MOV 0x88(RBP), RDX MOV 0x90(RBP), RBX ADD RBX, RDX MOV RDX, 0x80(RBP) # 次のADD命令で生成されるx86命令 MOV 0x80(RBP), RDX MOV 0x70(RBP), RBX ADD RBX, RDX MOV RDX, 0x78(RBP)
となる。ここでポイントは0x80(RBP)
をストアしてからロードするという無駄ことをしているということ。RDXを再利用するのであれば、レジスタに格納するためのストアMOVは必要だとしても再ロードは必要ない。これを省略するためには、現在x86のレジスタがRISC-Vのレジスタのどの値を保持しているかをテーブルで管理しておく必要がある。新しいレジスタロードにおいて、すでにx86命令レジスタ上に値を保持していればロード自体を省略する、ということになる。このためのテーブルを作成して、RISC-Vレジスタの生存範囲の管理を行う。
# 最初のADD命令で生成されるx86命令 MOV 0x88(RBP), RDX MOV 0x90(RBP), RBX ADD RBX, RDX MOV RDX, 0x80(RBP) # 次のADD命令で生成されるx86命令 # ここに存在していたMOVロード命令は不要なので省略する MOV 0x70(RBP), RBX ADD RBX, RDX MOV RDX, 0x78(RBP)
ただしこれもいろいろと制約があって、例えば上記の例だとRDXを別のレジスタが使用してしまえばこの最適化は使用できない。例えば、
ADD x10, x11, x12 ADD x9, x10, x8 ADD x11, x10, x13
とすると、上記の例だと2番目のADD命令においてRDXはx10からx9に置き換えられてしまい、3番目のADD命令実行の際にx10はロードし直す必要が生じてしまう。
# 最初のADD命令で生成されるx86命令 MOV 0x88(RBP), RDX MOV 0x90(RBP), RBX ADD RBX, RDX MOV RDX, 0x80(RBP) # 次のADD命令で生成されるx86命令 # MOV 0x80(RBP), RDX このロード命令は最適化のため省略される MOV 0x70(RBP), RBX ADD RBX, RDX MOV RDX, 0x78(RBP) # 3番目のADD命令で生成されるx86命令 MOV 0x80(RBP), RDX # このロード命令はRDXが書きつぶされてしまったため再ロードの必要がある MOV 0x98(RBP), RBX ADD RBX, RDX MOV RDX, 0x78(RBP)
これでは最適化の範囲があまり広くないため、もう少し頑張ってなるべく多くのx86レジスタを使用するようにする。
# 最初のADD命令で生成されるx86命令 MOV 0x88(RBP), RDX # x11はRDXが使用する MOV 0x90(RBP), RBX # x12はRBXが使用する ADD RBX, RDX # x10はRDXが使用する MOV RDX, 0x80(RBP) # x10はRDXが使用する # 次のADD命令で生成されるx86命令 # MOV 0x80(RBP), RDX すでにx10はRDXが使用しているため省略 MOV 0x70(RBP), RCX # x8はRCXが使用する ADD RDX, RCX # x9はRCXが使用する MOV RCX, 0x78(RBP) # x9はRCXが使用する # 3番目のADD命令で生成されるx86命令 # MOV 0x80(RBP), RDXすでにx10はRDXが使用しているため省略 MOV 0x98(RBP), SIB # x13はSIBが使用する ADD RDX, SIB # x11はSIBが使用する MOV RDX, 0x78(RBP) # SIBはx11が使用する
と、このように、動的にどのレジスタを使用するのかチェックしながらレジスタ割り当てを行う必要がある。このための配置のためのコストは考え直さなければなあ。
このときに、いくつか注意しなければならない点があった。まずは明示的にレジスタが使用される命令。
例えばIDIV命令などは、RDXとRAXを明示的に使用するため、IDIV命令の際にはこれらのオペランドが自動的に割り当てられるのを避けなければならない。
また、メモリアクセス命令にしてもr8モードでロードを実行する場合は仕様できるレジスタの種類が削減されるので注意する。むしろこの場合は通常の64ビットメモリアクセス命令でロードして、値をマスクしてシフトした方が手っ取り早いのかもしれない。