FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (48. 浮動小数点メモリアクセスの追加)

f:id:msyksphinz:20190425001356p:plain

では、まずは簡単な浮動小数点命令の実装から入る。 演算命令を定義するところから始めてもよいが、そのまえに必ずロードストア命令が必要になるので、ロードストア命令から入る。

RISC-Vでは、単精度浮動小数点用のFLW / FSW、倍精度浮動小数点用のFLD / FSD命令が定義されている。まずはこれらを定義する。

整数ロードストア命令のためにTarget DescriptionのクラスとしてLoadM32 / StoreM32を定義していた。 このクラスは取り扱うデータを整数に限定(つまりレジスタはGPRのみ)にしていたので、一般化して任意のレジスタクラスに対してデータを定義できるように変更する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td(差分)
 //@ 32-bit load.
-multiclass LoadM32<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode,
+multiclass LoadM32<bits<7> opcode, bits<3> funct3, string instr_asm,
+                   RegisterClass RC, PatFrag OpNode,
                    bit Pseudo = 0> {
-  def #NAME# : LoadM<opcode, funct3, instr_asm, OpNode, GPR, Pseudo>;
+  def #NAME# : LoadM<opcode, funct3, instr_asm, OpNode, RC, Pseudo>;
 }

 // 32-bit store.
-multiclass StoreM32<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode,
+multiclass StoreM32<bits<7> opcode, bits<3> funct3, string instr_asm,
+                    RegisterClass RC, PatFrag OpNode,
                     bit Pseudo = 0> {
-  def #NAME# : StoreM<opcode, funct3, instr_asm, OpNode, GPR, Pseudo>;
+  def #NAME# : StoreM<opcode, funct3, instr_asm, OpNode, RC, Pseudo>;
 }

これまでGPRと決め打ちしていたレジスタクラスを、RCとして一般化した。 これに伴い、LoadM32 / StoreM32を使用するクラスの定義にレジスタクラスを追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td(差分)
@@ -495,14 +500,14 @@ let isReturn=1, isTerminator=1, hasDelaySlot=0, isBarrier=1, hasCtrlDep=1 in

 /// Load and Store Instructions
 ///  aligned
-defm LW     : LoadM32 <0b0000011, 0b010, "lw",  load_a>;
-defm SW     : StoreM32<0b0100011, 0b010, "sw",  store_a>;
-defm LB     : LoadM32 <0b0000011, 0b000, "lb",  sextloadi8_a>;
-defm LBU    : LoadM32 <0b0000011, 0b100, "lbu", zextloadi8_a>;
-defm SB     : StoreM32<0b0100011, 0b000, "sb",  truncstorei8_a>;
-defm LH     : LoadM32 <0b0000011, 0b001, "lh",  sextloadi16_a>;
-defm LHU    : LoadM32 <0b0000011, 0b101, "lhu", zextloadi16_a>;
-defm SH     : StoreM32<0b0100011, 0b001, "sh",  truncstorei16_a>;
+defm LW     : LoadM32 <0b0000011, 0b010, "lw",  GPR, load_a>;
+defm SW     : StoreM32<0b0100011, 0b010, "sw",  GPR, store_a>;
+defm LB     : LoadM32 <0b0000011, 0b000, "lb",  GPR, sextloadi8_a>;
+defm LBU    : LoadM32 <0b0000011, 0b100, "lbu", GPR, zextloadi8_a>;
+defm SB     : StoreM32<0b0100011, 0b000, "sb",  GPR, truncstorei8_a>;
+defm LH     : LoadM32 <0b0000011, 0b001, "lh",  GPR, sextloadi16_a>;
+defm LHU    : LoadM32 <0b0000011, 0b101, "lhu", GPR, zextloadi16_a>;
+defm SH     : StoreM32<0b0100011, 0b001, "sh",  GPR, truncstorei16_a>;

これに追加する形で、FLW / FSW / FLD / FSDの定義を追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td
// Floating Point Load Store
defm FLW    : LoadM32 <0b0000111, 0b010, "flw", FPR_S, load_a >;
defm FSW    : StoreM32<0b0100111, 0b010, "fsw", FPR_S, truncstoref32_a>;

defm FLD    : LoadM32 <0b0000111, 0b011, "fld", FPR_D, load_a >;
defm FSD    : StoreM32<0b0100111, 0b011, "fsd", FPR_D, truncstoref64_a>;

ここでは、store_aではなくtruncstoref32_aおよびtruncstoref64_aというノードを使用している。 store_astoreノードを継承しているのだが、storeノードは整数用のノードなので、浮動小数点用の別のノードを使用する必要がある。

f:id:msyksphinz:20190801230619p:plain
RV32FとRV64DのLoad/Store命令を定義するにあたり、レジスタの種類を変える
  • llvm-myriscvx/include/llvm/Target/TargetSelectionDAG.td
def store : PatFrag<(ops node:$val, node:$ptr),
                    (unindexedstore node:$val, node:$ptr)> {
  let IsStore = 1;
  let IsTruncStore = 0;
}
...
// 浮動小数点用のtruncstoreが存在する。
def truncstoref32 : PatFrag<(ops node:$val, node:$ptr),
                            (truncstore node:$val, node:$ptr)> {
  let IsStore = 1;
  let MemoryVT = f32;
}
def truncstoref64 : PatFrag<(ops node:$val, node:$ptr),
                            (truncstore node:$val, node:$ptr)> {
  let IsStore = 1;
  let MemoryVT = f64;
}

これで命令の定義は完了したが、これだけでは足りない。 DAGのパタンに合わせて命令を生成する記述を追加する。これも整数命令を元に追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td
def : Pat<(f32 (load GPR:$rs1))                          , (FLW GPR:$rs1, 0)           >;
def : Pat<(f32 (load addr_fi:$rs1))                      , (FLW addr_fi:$rs1, 0)       >;
def : Pat<(f32 (load (add GPR:$rs1, simm12:$simm12)))    , (FLW GPR:$rs1, $simm12)     >;
def : Pat<(f32 (load (add addr_fi:$rs1, simm12:$simm12))), (FLW addr_fi:$rs1, $simm12) >;

def : Pat<(store FPR_S:$rs2, GPR:$rs1)                          , (FSW FPR_S:$rs2, GPR:$rs1, 0)                         >;
def : Pat<(store FPR_S:$rs2, addr_fi:$rs1)                      , (FSW FPR_S:$rs2, addr_fi:$rs1, 0)                     >;
def : Pat<(store FPR_S:$rs2, (add GPR:$rs1, simm12:$simm12))    , (FSW FPR_S:$rs2, GPR:$rs1, simm12:$simm12)            >;
def : Pat<(store FPR_S:$rs2, (add addr_fi:$rs1, simm12:$simm12)), (FSW FPR_S:$rs2, addr_fi:$rs1, simm12:$simm12)        >;

def : Pat<(f64 (load GPR:$rs1))                          , (FLD GPR:$rs1, 0)           >;
def : Pat<(f64 (load addr_fi:$rs1))                      , (FLD addr_fi:$rs1, 0)       >;
def : Pat<(f64 (load (add GPR:$rs1, simm12:$simm12)))    , (FLD GPR:$rs1, $simm12)     >;
def : Pat<(f64 (load (add addr_fi:$rs1, simm12:$simm12))), (FLD addr_fi:$rs1, $simm12) >;

def : Pat<(store FPR_D:$rs2, GPR:$rs1)                          , (FSD FPR_D:$rs2, GPR:$rs1, 0)                         >;
def : Pat<(store FPR_D:$rs2, addr_fi:$rs1)                      , (FSD FPR_D:$rs2, addr_fi:$rs1, 0)                     >;
def : Pat<(store FPR_D:$rs2, (add GPR:$rs1, simm12:$simm12))    , (FSD FPR_D:$rs2, GPR:$rs1, simm12:$simm12)            >;
def : Pat<(store FPR_D:$rs2, (add addr_fi:$rs1, simm12:$simm12)), (FSD FPR_D:$rs2, addr_fi:$rs1, simm12:$simm12)        >;

では、ここまで来たらLLVMのビルドを行い、テストプログラムを動かしてみる。以下のような簡単なコードを考える。

  • fp_mem.cpp
void fp_memoy(float *dst, float *src)
{
  *dst = *src;
}
void dp_memoy(double *dst, double *src)
{
  *dst = *src;
}

これをコンパイルしてみる。

./bin/clang -O0 fp_mem.cpp -emit-llvm
./bin/llc -filetype=asm fp_mem.bc -mcpu=simple32 -march=myriscvx32 -target-abi=lp64 -o -

float側のアセンブリだ。正しくflwfswが生成されていることが分かる。

_Z8fp_memoyPfS_:
# %bb.0:                                # %entry
        addi    x2, x2, -16
        sw      x10, 8(x2)
        sw      x11, 0(x2)
        lw      x10, 0(x2)
        flw     f0, 0(x10)
        lw      x10, 8(x2)
        fsw     f0, 0(x10)
        addi    x2, x2, 16
        ret

double側のアセンブリだ。正しくfldfsdが生成されていることが分かる。

_Z8dp_memoyPdS_:
# %bb.0:                                # %entry
        addi    x2, x2, -16
        sw      x10, 8(x2)
        sw      x11, 0(x2)
        lw      x10, 0(x2)
        fld     f0, 0(x10)
        lw      x10, 8(x2)
        fsd     f0, 0(x10)
        addi    x2, x2, 16
        ret