FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

自作Binary Translation型RISC-VエミュレータのTCG最適化試行

自作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
  1. x11の値をx86の任意のレジスタにロードする。(MOV 0x88(RBP), RDX)
  2. x12の値をx86の任意のレジスタにロードする。(MOV 0x90(RBP), RBX)
  3. 加算をする (ADD RDX, RBX)
  4. 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ビットメモリアクセス命令でロードして、値をマスクしてシフトした方が手っ取り早いのかもしれない。