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

LLVMのバックエンドを作るための第一歩 (47. 浮動小数点レジスタの定義)

f:id:msyksphinz:20190425001356p:plain

前章まででMYRISCVXの整数命令に関してLLVMバックエンドへの実装を進めてきた。 これだけでも随分と完成度が上がってきたが、まだ足りていないものがある。その一つとして、浮動小数点命令のサポートだ。

RISC-Vでは現状では2種類の浮動小数点命令が定義されている。

それぞれ、接尾語に.S.Dが付けられており、たとえば、

  • fmadd.s rd,rs1,rs2,rs3 : 単精度FMA演算命令 f[rd] = f[rs1]×f[rs2]+f[rs3]
  • fmadd.d rd,rs1,rs2,rs3 : 倍精度FMA演算命令 f[rd] = f[rs1]×f[rs2]+f[rs3]

などが定義されている。ここでは、これらの浮動小数点命令を追加してみる。

浮動小数レジスタを定義

まずは、浮動小数レジスタを定義しないと始まらない。 RISC-Vの浮動小数レジスタは、実体としては単精度版と倍精度版が共有されている。 つまり、倍精度をサポートしているRV64Dのアーキテクチャならば64ビットのレジスタが32本あり、下位の32ビットが単精度レジスタと共有されている。 一方、単精度をサポートしているRV32Fのみのサポートで、RV64Dをサポートしていないようなアーキテクチャならば、倍精度命令はそのままでは実行できない。 エミュレートなり、別の方法を模索する必要がある。

ここではRV32F用の単精度レジスタ、RV64D用の倍精度レジスタを定義する。しかし、これは共有関係にあるので、以下のような実装方針を取る。

  1. 単精度浮動小数レジスタ32本をこれまで通り定義する。
  2. 倍精度浮動小数レジスタ32本を定義するが、64ビットのうち下位の32ビットを単精度レジスタと共有するようにサブレジスタ指定を行う。

このようなサブレジスタによる共有は、冷静に考えてみると必須の機能だ。 X86ではAX, EAXレジスタなどのレジスタフィールドが共有されれているレジスタは沢山あるので、このようなレジスタ共有の機能があるのは当然だ。

f:id:msyksphinz:20190801225023p:plain
RV32FとRV64Dのレジスタをサブレジスタとして定義する仕組み

というわけで、まずは単精度浮動小数レジスタを定義する。MYRISCVXRegisterInfo.tdに以下の定義を追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td
// Floating point registers
let Namespace = "MYRISCVX" in {
  def F0_S  : MYRISCVXGPRReg< 0,  "f0", ["ft0" ]>, DwarfRegNum<[32]>;
  def F1_S  : MYRISCVXGPRReg< 1,  "f1", ["ft1" ]>, DwarfRegNum<[33]>;
  def F2_S  : MYRISCVXGPRReg< 2,  "f2", ["ft2" ]>, DwarfRegNum<[34]>;
...
  def F29_S : MYRISCVXGPRReg<29, "f29", ["ft9" ]>, DwarfRegNum<[61]>;
  def F30_S : MYRISCVXGPRReg<30, "f30", ["ft10"]>, DwarfRegNum<[62]>;
  def F31_S : MYRISCVXGPRReg<31, "f31", ["ft11"]>, DwarfRegNum<[63]>;
}
...
// Single Floating Point Register Class
def FPR_S : RegisterClass<"MYRISCVX", [f32], 32, (add
  F0_S , F1_S,  F2_S,  F3_S,  F4_S,  F5_S,  F6_S,  F7_S,
  F8_S , F9_S,  F10_S, F11_S, F12_S, F13_S, F14_S, F15_S,
  F16_S, F17_S, F18_S, F19_S, F20_S, F21_S, F22_S, F23_S,
  F24_S, F25_S, F26_S, F27_S, F28_S, F29_S, F30_S, F31_S
  )>;

MYRISCVXGPRRegクラスを使用して単精度レジスタを32本定義する。 整数レジスタの定義とあまり変わらないが、DwarfRegNumの定義のみ少し注意が必要だ。 ‘DwarfRegNumGCCGDBレジスタを参照するための一意に決められたレジスタ番号なので、整数レジスタと番号が被ってはいけない。 したがって、単精度レジスタ32本では32-63までの番号を使っている。

次に、倍精度浮動小数レジスタを定義する。上記で説明した通り、サブレジスタ指定を使って単精度レジスタを包含する。 このために、MYRISCVXFPR_Dレジスタクラスを作成する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td
  def sub_32 : SubRegIndex<32>;
  // MYRISCVX Double Floating-Point Registers
  class MYRISCVXFPR_D<bits<5> Enc, list<string> alt,
                      MYRISCVXGPRReg subreg> : Register<""> {
    let HWEncoding{4-0} = Enc;
    let SubRegs  = [subreg];
    let SubRegIndices = [sub_32];
    let AsmName  = subreg.AsmName;
    let AltNames = subreg.AltNames;
  }

要点としては以下だ。

  • MYRISCVXFPR_DRegisterクラスを継承する。ただし、その名前AsmRegisterはとりあえず空白""とする。
  • ハードウェエンコーディングHWEncodingは指定したものをそのまま使用する。
  • サブレジスタとしてSubRegsを指定する。これは配列を指定できるので、[subreg]として配列を指定する。subregは倍精度浮動小数レジスタ定義時に指定する。
  • レジスタAsmNameと代替レジスタAltNamesをここで指定する。これは、subregのものをそのまま使用する。

MYRISCVXFPR_Dクラスを利用して、倍精度レジスタを定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXRegisterInfo.td
...
  def F0_D  : MYRISCVXFPR_D< 0, ["ft0" ], F0_S >, DwarfRegNum<[64]>;
  def F1_D  : MYRISCVXFPR_D< 1, ["ft1" ], F1_S >, DwarfRegNum<[65]>;
  def F2_D  : MYRISCVXFPR_D< 2, ["ft2" ], F2_S >, DwarfRegNum<[66]>;
...
  def F29_D : MYRISCVXFPR_D<29, ["ft9" ], F29_S>, DwarfRegNum<[93]>;
  def F30_D : MYRISCVXFPR_D<30, ["ft10"], F30_S>, DwarfRegNum<[94]>;
  def F31_D : MYRISCVXFPR_D<31, ["ft11"], F31_S>, DwarfRegNum<[95]>;

32本分の倍精度レジスタを定義し、それぞれサブレジスタとして単精度レジスタを定義した。

ただし、これだけだとコンパイル時にエラーとなる。これは、現状の設定だとレジスタAltNameが重複する定義を許さないためだ。 現状では単精度レジスタf0-f31、倍精度レジスタf0-f31という名前で定義されており、レジスタ名が重複している。

LLVM ERROR: Had duplicate keys to match on

そこで、MYRISCVX.tdレジスタの重複を許可する設定を追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVX.td
def MYRISCVXAsmParser : AsmParser {
  let ShouldEmitMatchRegisterAltName = 1;
  let AllowDuplicateRegisterNames = 1;
}

def MYRISCVX : Target {
  let InstructionSet = MYRISCVXInstrInfo;
  let AssemblyParsers = [MYRISCVXAsmParser];
}

AllowDuplicateRegisterNamesを1に設定することで、レジスタ名が重複してもエラーを出さない。これで最後までコンパイルが通るようになる。

高性能プロセッサの分岐予測のサーベイ論文を読んで分岐予測について学ぶ (7. 予測精度を上げるためのテクニック2)

プロセッサアーキテクチャについて再度復習その7。分岐予測の続き。

読んでいるのはA Survey of Techniques for Dynamic Branch Predictionという論文で、新規技術の解説ではないのだが、現在の有名どころの分岐予測技術についてまんべんなく解説してくれている論文だ。

  • A Survey of Techniques for Dynamic Branch Prediction
    • Sparsh Mittal

https://arxiv.org/pdf/1804.00261.pdf


6. 分岐予測の精度を上げるための技術

6.3 カーネルモード実行でのエイリアシングの削減

これまでの提案手法は、すべてユーザモードでの測定であったが、Chenらの提案[70]では、

  • ユーザーモードの命令が全命令の95%以上を占める場合、ユーザーのみとシステム全体の誤予測率はほぼ一致する。

  • ユーザーモードの指示が90%未満の場合、システム全体の結果が反映されない。

したがって、ユーザー専用の分岐トレースに対する最適な分岐予測器は、フルシステムトレースに対するものと同じではない可能性がある。さらに、カーネル分岐を含めると、予測される静的分岐の数が増えるため、エイリアシングが悪化する。これにより、分岐予測器の有効サイズが小さくなる。エイリアスの影響は、ローカル深度の短い分岐予測器を使用する分岐予測器よりも、長い履歴を持つ2レベルの分岐予測器の方が高くなる。さらに、Nair et al。によって示唆されているように、定期的に分岐履歴をフラッシュする。 [26]は、カーネルや他のプロセスが常に分岐履歴状態をフラッシュするとは限らないため、ユーザーとカーネルの対話の影響を正確にはモデル化していない。また、フラッシュは大きなテーブルサイズの分岐予測器にとって特に有害である。

Liらの提案[71]に示すところでは、ユーザコードとカーネルコードの分岐の傾向はそれぞれ異なるので、それぞれが同時に実行されると分岐エイリアシングにより分岐ミスが増大してしまう。また、カーネルコードの多くは、分岐の傾向を把握することが難しい。これは分岐予測器のバッファのサイズを大きくしても問題は解決しない。

そこで、彼らの提案では、2つの手法を紹介している。

  1. 2つの分岐履歴レジスタを用意し、ユーザコードとカーネルコードの分岐情報を分離する。
  2. 1の手法ではPHT内で再度ユーザ・カーネルエイリアシングが発生するので、分岐履歴レジスタとPHTも分離してしまう。
    • カーネルコード内のアクティブな分岐の数はユーザコードの分岐の情報よりも少ないため、カーネルモードのPHTはユーザモードのPHTよりも小さくできる。

6.4 コンフリクトが削減されるインデックス関数の使用

Maらの提案[88]では、分岐予測器のテーブルエイリアシングを削減するためにインデックス関数を評価する。評価に使用したインデックス関数は表7に示している。

マッピング手法 インデックス関数
Modulo  \text{index} = x = A \mod 2^ k
Bitwise-XOR  \text{index} = x \oplus y
規約多項式(Irreducible Polynomial) インデックスの2進数表記 :  R(x) = A(x) \mod P(x), R(x) = a _ {n-1} R _ {n-1}(x) + \cdots +a _ 1 R _ 1(x) + a _ 0 R _ 0(x) ここで R_ i(x) = x^ I\mod P(x) は、規約多項式 P(x) の選択後に事前計算ができる。
Prime-Modulo  index = A\mod p \mod 2^ k  p 2 ^kに最も近い 2 ^kよりも小さな数。
A-prime-Modulo  index = A \mod p \mod 2^ k  pは最も小さいテーブルエントリ数以上の素数
Prime-displacement  index = (T * p + x) \mod 2^ k ,  p素数(例えば、17など)
  • 分岐予測器テーブルには2^ kのエントリがある。
  • Prime-Moduloマッピングはすべてのエントリを利用するわけではない。
  • A-Prime-Moduloマッピングでは、すべてのテーブルエントリが使用されるが、一部のエントリの使用頻度が高くなるという特徴がある。
  • 異なる分岐予測器バンクにおける異なるインデックス関数の使用を試行している。

結果:Moduloマッピングを基準とした場合、

  • gshare 分岐予測器 :
    • XORインデックスは一般的に精度を向上させる。
    • 既約多項式関数は精度が下がる。
    • Prime-Moduloは常に高い精度を提供できるわけではなかった。
    • Prime-displacementは大きな分岐予測器テーブルに対してはXORマッピングよりも優れた正答率を示すが、小さなテーブルに対しては正答率は低い。
  • パーセプトロン分岐予測器
    • すべてのインデックス関数でエイリアシングが大幅に減少した。
    • 小さなテーブルではエイリアシングが大きくなるため、大きなテーブルよりも小さなテーブルのほうが改善率が高くなる。
    • どのようなインデックス関数でも、様々なテーブルサイズにおいて一般的に性能が良い。
    • 大きなテーブルにXORマッピングを使用し、小さなテーブルにPrime-Displacementを使用すると、高い精度を達成しながら複雑さを低く抑えることができる。
    • 高度なインデックス関数が使用されている場合でも、タグを使うことによって除去可能なエイリアシングはまだ存在している。
    • 複数のインデックス関数を使用することも、エイリアシングを減らすのに効果的であることが分かった。

6.5 マルチスレッドへの対応とショートスレッド

スレッド数が増えるにつれて、分岐予測器に必要なテーブルサイズはより大きくなる。

Hilyら[96]の提案。マルチスレッドのワークロードで、各ハードウェアスレッドコンテキスト必要なRASエントリの最大値は12エントリで、それ以上大きくしても効果がない。

  • ワークロード内の異なるアプリケーションにデータ共有がない(マルチプログラムのワークロード)

    • スレッド数を使用してPHT/BTBテーブルのサイズを調整すると、すべてのエイリアスが削除することができる。
  • データ共有を伴う並列ワークロード

    • gshareおよびgselectでは、共有効果によりわずかに精度が向上するが、それ以外の分岐予測器(例えば、2BC)は、スレッド数の増加と共に予測精度が落ちる。

どちらの種類のワークロードでも、BTBのサイズを削減すると、BTBでの競合により予測ミスの数が増える。

Choiら [95]の提案。GHRはスレッド毎に履歴情報を格納していないため、大きな制御フロー履歴を処理している分岐予測器は細かく切り替えが行われるスレッドに対してはうまく機能しない。

  • 提案 : 現在または過去の情報を用いて、予想されるGHRを予測または再現する手法。
    • たとえば、子スレッドは親スレッドのGHRを継承する。
    • 同じスレッドが始まるときはいつでも分岐予測器のための一貫したスタートポイントを提供するだけである。
    • 例えば、投機的スレッドの最初の命令のPCからGHRを作成することによって、一意の初期GHR状態を各スレッドに提供することができる。これらのテクニックが短いスレッドのための誤予測を減らすことによってパフォーマンスを改善することを示した。

6.6 誤ったパス実行の情報を利用する

Akkaryらの提案[97]。制御が独立している場合、間違ったパス上の分岐結果は正しいパス上の分岐結果と一致する。

ただし、現在の分岐予測器は正しいパスでのみブランチ相関を研究するため、誤ったパスの情報を使用しない。彼らの技術は、gshare分岐予測器 [81]と同様のアルゴリズムを使用して、正しいパスと間違ったパスでの分岐の実行の間に相関の存在を見つける。

レベル1では、正しい経路で観測された最後のM個の分岐の結果がBHRに格納される。図30に示すように、誤予測分岐のアドレスはM/2ビット左にシフトされる。次に、2ビット飽和カウンタのためのインデックス計算が行なわれる。

インデックスの計算 : 予測対象の正しいパス上の分岐からのMビットとシフトされた誤予測分岐アドレスからのMビットのに対してXORが取られる。

カウンタ値が1を超える場合、分岐とその誤った経路との相関が存在するとして、誤った経路の結果が、分岐予測器の予測の代わりに最終的に選択される。

f:id:msyksphinz:20190801004621p:plain

カウンタの更新方針について。

  • 正しいパスと間違ったパスの結果が一致し、分岐予測器の予測が誤った場合、カウンタが増加する。
  • 正しいパスと間違ったパスで結果が異なり、分岐予測器が正しく予測していれば、カウンターは減少する。
  • それ以外の場合、カウンタは変更されない。

間違ったパスでの分岐結果はタグ付きバッファに格納される。このバッファ内の分岐結果はせいぜい1回使用されてから追い出されるので、命令ウィンドウ内の分岐のみに対応する必要がある。複数の誤った経路の結果を記憶してもわずかな改善しか得られないため、最新の誤った経路の結果のみが維持される。彼らの技術は、予測ミスを大幅に減らし、パフォーマンスを向上させた。彼らの技術の限界は、それが正しい経路と間違った経路で異なる結果を持つブランチの精度を向上させることができないということである。

LLVMのバックエンドを作るための第一歩 (46. LR/SCでアトミック操作を実現することを考える)

f:id:msyksphinz:20190425001356p:plain

LR/SCを用いたAtomicコードの生成

以下のようなコードを考える。

  • cpp_atomic.cpp
#include <stdint.h>
#include <atomic>

std::atomic<int32_t> x32;
std::atomic<int16_t> x16;
std::atomic<int8_t > x8;

int32_t test_32()
{
  x32.fetch_add(2);
  return x32.load();
}

int16_t test_16()
{
  x16.fetch_add(2);
  return x16.load();
}

int8_t test_8()
{
  x8.fetch_add(2);
  return x8.load();
}

3種類用意した。32ビット版、16ビット版、8ビット版でそれぞれアトミック操作を行い加算を行うプログラムだ。アトミックな変数はグローバルな領域に確保しており、それをアトミックに加算して、その結果を返す仕組みだ。

まず1つ目の作戦として、MIPSのコードを真似てLoad Linked / Store Conditionalを使って実装してみる。MIPSではLL/SC命令という命令がありる。Load Linkedでリンク付きのロードを行い、Store Conditionalで、リンクがまだ切れていなければ正常なストア、別のスレッドにより書き換えが生じている場合は失敗として何度もLLから再試行する。

RISC-Vにも同様の命令が定義されている。LR(Load-Reserved)とSC(Store-Conditional)だ。

f:id:msyksphinz:20190731235420p:plain

32ビット整数レジスタ(RV32)に対しては、LR.WSC.W命令が定義されている。64ビットに対しては、LR.D命令とSC.D命令が定義されている。ここでは、LR.WSC.W命令を定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
class LRBase<bits<7> opcode, bits<3> funct3, bits<7> funct7, string opstring, RegisterClass RC, Operand Mem> :
  MYRISCVX_R<opcode, funct3, funct7, (outs RC:$rd), (ins RC:$rs1),
     !strconcat(opstring, "\t$rd, (${rs1})"), [], IILoad> {
  let mayLoad = 1;
  let rs2 = 0;
}

class SCBase<bits<7> opcode, bits<3> funct3, bits<7> funct7, string opstring, RegisterClass RC, Operand Mem> :
  MYRISCVX_R<opcode, funct3, funct7, (outs RC:$rd), (ins RC:$rs1, RC:$rs2),
       !strconcat(opstring, "\t$rd, $rs2, (${rs1})"), [], IIStore> {
  let mayStore = 1;
}

/// Load-linked, Store-conditional
def LR_W : LRBase<0b0101111, 0b010, 0b0001000, "lr.w", GPR, mem>;
def SC_W : SCBase<0b0101111, 0b010, 0b0001100, "sc.w", GPR, mem>;

LRBaseはLoad Reserved用のテンプレートだ。rs2レジスタフィールドは0なので、let rs2 = 0;で固定している。SCBaseはStore Conditional用のテンプレートだ。

この2つを使って、今回はLR_WSC_Wを定義した。

次に、Atomic操作を定義する。実際の動作はMYRISCVXTargetLoweringクラス内で実装することにする。

MYRISCVXTargetLoweringクラス内のEmitInstrWithCustomInserter()に実装を追加するので、Atomic操作の定義は、useCustomInserter =で囲む。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Atomic instructions with 2 source operands (ATOMIC_SWAP & ATOMIC_LOAD_*).
class Atomic2Ops<PatFrag Op, RegisterClass DRC> :
  MYRISCVXPseudo<(outs DRC:$rd), (ins PtrRC:$ptr, DRC:$incr), "",
                  [(set DRC:$rd, (Op iPTR:$ptr, DRC:$incr))]>;

// Atomic Compare & Swap.
class AtomicCmpSwap<PatFrag Op, RegisterClass RC> :
  MYRISCVXPseudo<(outs RC:$rd), (ins PtrRC:$ptr, RC:$cmp, RC:$swap), "",
                  [(set RC:$rd, (Op iPTR:$ptr, RC:$cmp, RC:$swap))]>;
                
let usesCustomInserter = 1 in {
  def ATOMIC_LOAD_ADD_I8   : Atomic2Ops<atomic_load_add_8     , GPR>;
  def ATOMIC_LOAD_ADD_I16  : Atomic2Ops<atomic_load_add_16    , GPR>;
  def ATOMIC_LOAD_ADD_I32  : Atomic2Ops<atomic_load_add_32    , GPR>;
  def ATOMIC_LOAD_SUB_I8   : Atomic2Ops<atomic_load_sub_8     , GPR>;
  def ATOMIC_LOAD_SUB_I16  : Atomic2Ops<atomic_load_sub_16    , GPR>;
  def ATOMIC_LOAD_SUB_I32  : Atomic2Ops<atomic_load_sub_32    , GPR>;
  def ATOMIC_LOAD_AND_I8   : Atomic2Ops<atomic_load_and_8     , GPR>;
  def ATOMIC_LOAD_AND_I16  : Atomic2Ops<atomic_load_and_16    , GPR>;
  def ATOMIC_LOAD_AND_I32  : Atomic2Ops<atomic_load_and_32    , GPR>;
  def ATOMIC_LOAD_OR_I8    : Atomic2Ops<atomic_load_or_8      , GPR>;
  def ATOMIC_LOAD_OR_I16   : Atomic2Ops<atomic_load_or_16     , GPR>;
  def ATOMIC_LOAD_OR_I32   : Atomic2Ops<atomic_load_or_32     , GPR>;
  def ATOMIC_LOAD_XOR_I8   : Atomic2Ops<atomic_load_xor_8     , GPR>;
  def ATOMIC_LOAD_XOR_I16  : Atomic2Ops<atomic_load_xor_16    , GPR>;
  def ATOMIC_LOAD_XOR_I32  : Atomic2Ops<atomic_load_xor_32    , GPR>;
  def ATOMIC_LOAD_NAND_I8  : Atomic2Ops<atomic_load_nand_8    , GPR>;
  def ATOMIC_LOAD_NAND_I16 : Atomic2Ops<atomic_load_nand_16   , GPR>;
  def ATOMIC_LOAD_NAND_I32 : Atomic2Ops<atomic_load_nand_32   , GPR>;

  def ATOMIC_SWAP_I8       : Atomic2Ops<atomic_swap_8         , GPR>;
  def ATOMIC_SWAP_I16      : Atomic2Ops<atomic_swap_16        , GPR>;
  def ATOMIC_SWAP_I32      : Atomic2Ops<atomic_swap_32        , GPR>;

  def ATOMIC_CMP_SWAP_I8   : AtomicCmpSwap<atomic_cmp_swap_8  , GPR>;
  def ATOMIC_CMP_SWAP_I16  : AtomicCmpSwap<atomic_cmp_swap_16 , GPR>;
  def ATOMIC_CMP_SWAP_I32  : AtomicCmpSwap<atomic_cmp_swap_32 , GPR>;
}

実装の方針としては、

  • Atomic操作を定義する。定義する操作は、

    • 加算(ADD), 減算(SUB), 論理AND(AND), 論理OR(OR), 論理XOR(XOR), 論理NAND(NAND), スワップ(Swap)、比較とスワップ(Cmp_Swap)
  • 実装する関数は

    • 32ビットのAtomic操作 : MYRISCVXTargetLowering::emitAtomicBinary(), MYRISCVXTargetLowering::emitAtomicCmpSwap()で実装する。

    • 16ビットのAttmic操作 / 8ビットのAtomic操作 : MYRISCVXTargetLowering::emitAtomicBinaryPartword(), MYRISCVXTargetLowering::emitAtomicCmpSwapPartword()に実装する。基本的な考え方は32ビット版の実装と同じで、データをマスクして16ビット、8ビットに対応させる。

f:id:msyksphinz:20190801000326p:plain
CustomInserterを使ってアトミック操作を制御する。

というわけで、MYRISCVXTargetLowering::EmitInstrWithCustomInserter()に条件分岐を追加する。

MachineBasicBlock *
MYRISCVXTargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI,
                                                    MachineBasicBlock *BB) const {

  switch (MI.getOpcode()) {
    default:
      llvm_unreachable("Unexpected instr type to insert");
    case MYRISCVX::Select_GPR_Using_CC_GPR:
      break;
    case MYRISCVX::ATOMIC_LOAD_ADD_I8:   return emitAtomicBinaryPartword  (MI, BB, 1, MYRISCVX::ADD);
    case MYRISCVX::ATOMIC_LOAD_ADD_I16:  return emitAtomicBinaryPartword  (MI, BB, 2, MYRISCVX::ADD);
    case MYRISCVX::ATOMIC_LOAD_ADD_I32:  return emitAtomicBinary          (MI, BB, 4, MYRISCVX::ADD);

    case MYRISCVX::ATOMIC_LOAD_AND_I8:   return emitAtomicBinaryPartword  (MI, BB, 1, MYRISCVX::AND);
    case MYRISCVX::ATOMIC_LOAD_AND_I16:  return emitAtomicBinaryPartword  (MI, BB, 2, MYRISCVX::AND);
    case MYRISCVX::ATOMIC_LOAD_AND_I32:  return emitAtomicBinary          (MI, BB, 4, MYRISCVX::AND);

    case MYRISCVX::ATOMIC_LOAD_OR_I8:    return emitAtomicBinaryPartword  (MI, BB, 1, MYRISCVX::OR);
    case MYRISCVX::ATOMIC_LOAD_OR_I16:   return emitAtomicBinaryPartword  (MI, BB, 2, MYRISCVX::OR);
    case MYRISCVX::ATOMIC_LOAD_OR_I32:   return emitAtomicBinary          (MI, BB, 4, MYRISCVX::OR);

    case MYRISCVX::ATOMIC_LOAD_XOR_I8:   return emitAtomicBinaryPartword  (MI, BB, 1, MYRISCVX::XOR);
    case MYRISCVX::ATOMIC_LOAD_XOR_I16:  return emitAtomicBinaryPartword  (MI, BB, 2, MYRISCVX::XOR);
    case MYRISCVX::ATOMIC_LOAD_XOR_I32:  return emitAtomicBinary          (MI, BB, 4, MYRISCVX::XOR);

    case MYRISCVX::ATOMIC_LOAD_NAND_I8:  return emitAtomicBinaryPartword  (MI, BB, 1, 0, true);
    case MYRISCVX::ATOMIC_LOAD_NAND_I16: return emitAtomicBinaryPartword  (MI, BB, 2, 0, true);
    case MYRISCVX::ATOMIC_LOAD_NAND_I32: return emitAtomicBinary          (MI, BB, 4, 0, true);

    case MYRISCVX::ATOMIC_LOAD_SUB_I8:   return emitAtomicBinaryPartword  (MI, BB, 1, MYRISCVX::SUB);
    case MYRISCVX::ATOMIC_LOAD_SUB_I16:  return emitAtomicBinaryPartword  (MI, BB, 2, MYRISCVX::SUB);
    case MYRISCVX::ATOMIC_LOAD_SUB_I32:  return emitAtomicBinary          (MI, BB, 4, MYRISCVX::SUB);

    case MYRISCVX::ATOMIC_SWAP_I8:       return emitAtomicBinaryPartword  (MI, BB, 1, 0);
    case MYRISCVX::ATOMIC_SWAP_I16:      return emitAtomicBinaryPartword  (MI, BB, 2, 0);
    case MYRISCVX::ATOMIC_SWAP_I32:      return emitAtomicBinary          (MI, BB, 4, 0);

    case MYRISCVX::ATOMIC_CMP_SWAP_I8:   return emitAtomicCmpSwapPartword (MI, BB, 1);
    case MYRISCVX::ATOMIC_CMP_SWAP_I16:  return emitAtomicCmpSwapPartword (MI, BB, 2);
    case MYRISCVX::ATOMIC_CMP_SWAP_I32:  return emitAtomicCmpSwap         (MI, BB, 4);
  }

大量に条件分岐を追加してしまったが、要するに

  • 32ビットのAtomiLoadの場合はemitAtomicBinary()を呼ぶ。
  • 16ビット/8ビットのAtomicLoadの場合はemitAtomicBinaryPartword()を呼ぶ。
  • NANDの場合は条件が若干複雑 (RISC-VにNAND命令が存在しないので)。
  • 32ビットのAtomicCmpSwapemitAtomicCmpSwap()を呼び、16ビット/8ビットの場合はemitAtomicCmpSwapPartword()を呼ぶ。

emitAtomicBinary()の実装は以下のようになる。まず、LRを発行してSCを発行し、その結果を確認する。

  // insert new blocks after the current block
  const BasicBlock *LLVM_BB = BB->getBasicBlock();
  MachineBasicBlock *loopMBB = MF->CreateMachineBasicBlock(LLVM_BB);
  MachineBasicBlock *exitMBB = MF->CreateMachineBasicBlock(LLVM_BB);
  MachineFunction::iterator It = ++BB->getIterator();
  MF->insert(It, loopMBB);
  MF->insert(It, exitMBB);

  // Transfer the remainder of BB and its successor edges to exitMBB.
  exitMBB->splice(exitMBB->begin(), BB,
                  std::next(MachineBasicBlock::iterator(MI)), BB->end());
  exitMBB->transferSuccessorsAndUpdatePHIs(BB);

  //  thisMBB:
  //    ...
  //    fallthrough --> loopMBB
  BB->addSuccessor(loopMBB);
  loopMBB->addSuccessor(loopMBB);
  loopMBB->addSuccessor(exitMBB);

  //  loopMBB:
  //    ll oldval, 0(ptr)
  //    <binop> storeval, oldval, incr
  //    sc success, storeval, 0(ptr)
  //    beq success, $0, loopMBB
  BB = loopMBB;
  // Load Reservedを実行
  BuildMI(BB, DL, TII->get(LR), OldVal).addReg(Ptr);
  // 演算を実行
  BuildMI(BB, DL, TII->get(BinOpcode), StoreVal).addReg(OldVal).addReg(Incr);
  // Store Conditionalを実行
  BuildMI(BB, DL, TII->get(SC), Success).addReg(Ptr).addReg(StoreVal);
  // BEQで条件が成立しなければもう一度LRからやり直す。
  BuildMI(BB, DL, TII->get(BEQ)).addReg(Success).addReg(ZERO).addMBB(loopMBB);

コードを実行すると、以下のようになる。

_Z7test_32v:
        .cfi_startproc
        .frame  x8,8,x1
        .mask   0x00000004,-4
        .set    noreorder
        .set    nomacro
# %bb.0:                                # %entry
        addi    x2, x2, -8
        .cfi_def_cfa_offset 8
        sw      x2, 4(x2)               # 4-byte Folded Spill
        .cfi_offset 2, -4
        sync 0
        lui     x10, %hi(x32)
        ori     x10, x10, %lo(x32)
        addi    x11, zero, 2
$BB0_1:                                 # %entry
                                        # =>This Inner Loop Header: Depth=1
        lr.w    x12, (x10)
        add     x12, x12, x11
        sc.w    x12, x12, (x10)
        beq     x12, zero, $BB0_1
# %bb.2:                                # %entry
        sync 0
        addi    x11, zero, 0
        addi    x12, x11, 0
        j       __sync_val_compare_and_swap_4
        sync 0
        lw      x2, 4(x2)               # 4-byte Folded Reload
        addi    x2, x2, 8
        ret

LLVMのバックエンドを作るための第一歩 (45. Atomic命令を生成する)

f:id:msyksphinz:20190425001356p:plain

C++ではスレッド機能がサポートされており、ClangでもLLVM IRがサポートされている。例えば、以下のようなコードでIRを出力してみる。

  • cpp_atomic.cpp
#include <stdint.h>
#include <atomic>

int test_32()
{
  std::atomic<int> x(3);
  int before = x.fetch_add(2);
  return x.load();
}

今回は簡単化のため、-O3コンパイルする。余計な命令が出てこないので、こちらの方が見やすいからだ。

./bin/clang cpp_atomic.cpp -O3 -emit-llvm
./bin/llvm-dis cpp_atomic.bc -o -
; Function Attrs: nounwind uwtable
define dso_local i32 @_Z7test_32v() local_unnamed_addr #0 personality i32 (...)* @__gxx_personality_v0 {
entry:
  %x = alloca %"struct.std::atomic", align 4
  %0 = bitcast %"struct.std::atomic"* %x to i8*
  call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %0) #2
  %1 = getelementptr inbounds %"struct.std::atomic", %"struct.std::atomic"* %x, i64 0, i32 0, i32 0
  store i32 3, i32* %1, align 4
  %2 = atomicrmw add i32* %1, i32 2 seq_cst
  %3 = load atomic i32, i32* %1 seq_cst, align 4
  call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %0) #2
  ret i32 %3
}

atomicrmw addが生成されている。以下の表のように、IRを生成することができる。

IR DAG Opcode
load atomic AtomicLoad ATOMIC_CMP_SWAP_XXX
store atomic AtomicStore ATOMIC_SWAP_XXX
atomicrmw add AtomicLoadAdd ATOMIC_LOAD_ADD_XXX
atomicrmw sub AtomicLoadSub ATOMIC_LOAD_SUB_XXX
atomicrmw xor AtomicLoadXor ATOMIC_LOAD_XOR_XXX
atomicrmw and AtomicLoadAnd ATOMIC_LOAD_AND_XXX
atomicrmw nand AtomicLoadNand ATOMIC_LOAD_NAND_XXX
atomicrmw or AtomicLoadOr ATOMIC_LOAD_OR_XXX
cmpxchg AtomicCmpSwapWithSuccess ATOMIC_CMP_SWAP_XXX
atomicrmw xchg AtomicLoadSwap ATOMIC_SWAP_XXX

MYRISCVXInstrInfo.tdに、可能な限りの生成パタンを追加してみる。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
...
// Atomic instructions with 2 source operands (ATOMIC_SWAP & ATOMIC_LOAD_*).
class Atomic2Ops<bits<7> opcode, bits<3> funct3, bits<7>funct7,
                 string instr_asm, PatFrag Op, RegisterClass RC> :
  MYRISCVX_R<opcode, funct3, funct7, (outs RC:$rd), (ins PtrRC:$rs1, RC:$rs2),
             !strconcat(instr_asm, "\t$rd, $rs1, (${rs2})"),
             [(set RC:$rd, (Op iPTR:$rs1, RC:$rs2))], IILoad>;
...
def ATOMIC_LOAD_ADD_I32  : Atomic2Ops<0b0101111, 0b010, 0b0000000, "amoadd.w" , atomic_load_add_32    , GPR>;
def ATOMIC_LOAD_AND_I32  : Atomic2Ops<0b0101111, 0b010, 0b0110000, "amoand.w" , atomic_load_and_32    , GPR>;
def ATOMIC_LOAD_OR_I32   : Atomic2Ops<0b0101111, 0b010, 0b0100000, "amoor.w"  , atomic_load_or_32     , GPR>;
def ATOMIC_LOAD_XOR_I32  : Atomic2Ops<0b0101111, 0b010, 0b0010000, "amoxor.w" , atomic_load_xor_32    , GPR>;

def ATOMIC_SWAP_I32      : Atomic2Ops<0b0101111, 0b010, 0b0000100, "amoswap.w", atomic_swap_32        , GPR>;

RISC-Vのamoadd.w, amoand.w, amoor, amoxor.w, amoswap.wを定義してみた。

  • amo{Op}.w rd1, rs1, (rs2) : rs2に格納されているアドレスからデータをロードし、その値をrdに示されるレジスタに格納する。ロードした値に対してrs1の値をOpで示される演算を適用し、その結果をrs2で格納されれているアドレスにストアする。上記操作をアトミックに行う。

とりあえずこの状態でテストコードをコンパイルしてみる。

./bin/llc -filetype=asm cpp_atomic.bc -mcpu=simple32 -march=myriscvx32 -o -

生成したアセンブリを見てみる。

        addi    x2, x2, -8
        .cfi_def_cfa_offset 8
        sw      x2, 4(x2)               # 4-byte Folded Spill
        .cfi_offset 2, -4
        addi    x10, zero, 3
        sw      x10, 0(x2)
        addi    x11, zero, 2
        addi    x10, x2, 0
        amoadd.w        x11, x10, (x11)
        addi    x11, zero, 0
        addi    x12, x11, 0
        j       __sync_val_compare_and_swap_4
        lw      x2, 4(x2)               # 4-byte Folded Reload
        addi    x2, x2, 8
        ret

amoswap.wが生成されているのが分かる。

RISC-V Privileged ISAマニュアル20190608-Priv-MSU-Ratified版の変更点を読み解く

f:id:msyksphinz:20181016221815p:plain

"The RISC-V Instruction Set Manual. Volume II: Privileged ISA Document Version 20190608-Priv-MSU-Ratified" が公開されてしばらくたったのだが、少し時間ができたので変更点の部分を簡単に調べてみることにした。

riscv.org


各モードの状態について。

Module Version Status
Machine ISA 1.11 Ratified
Supervisor ISA 1.11 Ratified
Hypervisor ISA 0.3 Draft

Prefaceによる、仕様の改定部分は以下。

  • Machine ISAおよびSupervisor ISAの仕様を"Ratified"ステータスに変更。
  • 説明と解説の改善。
  • Hypervisor ISA拡張のDraftを追加。
  • どの割り込みソースが標準使用のために予約されているかを記載。
  • カスタム使用のためにいくつかの同期例外の原因の割り当て。
  • 同期例外の優先順位を記載。
  • "A"拡張が存在する場合、xRET命令がLR予約をクリアすることができるが
    • ただし必須ではない。
  • SUMの設定に関係なく、仮想メモリシステムは、スーパーバイザモードがユーザページからの命令を実行することを許可しないように変更。
  • 将来の拡張でASIDをグローバル化してパフォーマンスとハードウェアの柔軟性を向上させることができるように、ソフトウェアでASIDをグローバルに割り当てることを強く推奨する。
  • SFENCE.VMAのセマンティクスを明確化。
  • mstatus.MPPフィールドがWLRLからWARLに変更。
  • xipのうち未使用のフィールドをWIRIからWPRIに変更。
  • misaの未使用のフィールドをWIRIからWLRLに変更。
  • pmpaddrおよびpmpcfgの未使用のフィールドをWIRIからWARLに変更。
  • システム内のすべてのHartが互いに同じPTE更新スキームを採用する必要があることを記載。
  • 例外が発生したときにmstatus.xIEが書き込まれるメカニズムについて説明の誤りを修正。
  • ミスアラインした場合のAMOをエミュレートするためのスキームを説明。
  • 変数IALIGNを使用して、システムのmisaおよびxepcレジスタの動作を記載。
  • misaレジスタに矛盾する値を書き込む動作を指定。
  • mcountinhibit CSRを定義。これは、パフォーマンスカウンタのインクリメントを止めて、消費電力の増大を防ぐ。
  • 4バイトより粗いPMP領域のセマンティクスを記載。
  • XLENの変更に関するCSRの動作について記載。
  • PLICについての記載を分離。

新たな割り込み要因の追加

カスタム命令などで利用できる例外の要因の追加。

Interrupt Exception Code Description
1 0 User software interrupt
1 1 Supervisor software interrupt
1 2 Reserved for future standard use
1 3 Machine software interrupt
1 4 User timer interrupt
1 5 Supervisor timer interrupt
1 6 Reserved for future standard use
1 7 Machine timer interrupt
1 8 User external interrupt
1 9 Supervisor external interrupt
1 10 Reserved for future standard use
1 11 Machine external interrupt
1 12–15 Reserved for future standard use
1 ≥16 Reserved for platform use
0 0 Instruction address misaligned
0 1 Instruction access fault
0 2 Illegal instruction
0 3 Breakpoint
0 4 Load address misaligned
0 5 Load access fault
0 6 Store/AMO address misaligned
0 7 Store/AMO access fault
0 8 Environment call from U-mode
0 9 Environment call from S-mode
0 10 Reserved
0 11 Environment call from M-mode
0 12 Instruction page fault
0 13 Load page fault
0 14 Reserved for future standard use
0 15 Store/AMO page fault
0 16–23 Reserved for future standard use
0 24–31 Reserved for custom use
0 32–47 Reserved for future standard use
0 48–63 Reserved for custom use
0 ≥64 Reserved for future standard use

割り込み・例外の優先順位

f:id:msyksphinz:20190727194742p:plain

CSRの各フィールドの表記について

正常値書き込み 不正地書き込み 読み込み 備考
Reserved Writes Ignored, Reads Ignore Values (WIRI) 無視される 無視される デフォルト値が読まれる。 ソフトウェアは無視する必要がある。 将来の予約フィールド
Reserved Writes Preserve Values, Reads Ignore Values (WPRI) 読み込んだ値を保持するようにストアする必要がある 読み込んだ値を保持するようにストアする必要がある 書き込んだ値が読まれる 将来の予約フィールド
Write/Read Only Legal Values (WLRL) 正常書き込み 不正書き込み 正常に書いた場合は正常に読み込まれる。不正書き込み後は値は不定となる
Write Any Values, Reads Legal Values (WARL) 正常書き込み 不正書き込み 不正書き込みをしても正常な値が読まれる。

Machine Counter-Inhibit CSR(mcountinhibit)

f:id:msyksphinz:20190727194928p:plain
  • CYビット : cycleレジスタの制御。0でインクリメント、1でストップ
  • IRビット : instretレジスタの制御。0でインクリメント、1でストップ
  • HPMnビット : HPMレジスタの制御。0でインクリメント、1でストップ

RISC-V Unprivileged ISAマニュアル20190608-Base-Ratified版の変更点を読み解く

f:id:msyksphinz:20181016221815p:plain

"The RISC-V Instruction Set Manual. Volume I: Unprivileged ISA Document Version 20190608-Base-Ratified" が公開されてしばらくたったのだが、少し時間ができたので変更点の部分を簡単に調べてみることにした。

riscv.org


命令セットの分類は以下のように変更されている。

Base Version Status
RVWMO (RISC-Vメモリコンシステンシモデル) 2.0 Ratified
RV32I (32-bit RISC-VベースISA) 2.1 Ratified
RV64I (64-bit RISC-VベースISA) 2.1 Ratified
RV32E (32-bit RISC-V Embedded ISA) 1.9 Draft
RV128I (128-bit RISC-VベースISA) 1.7 Draft
Extension Version Status
Zifencei (命令フェッチ"Fence") 2.0 Ratified
Zicsr (Control and Status Register (CSR)命令) 2.0 Ratified
M (整数乗算) 2.0 Ratified
A (アトミック命令) 2.0 Frozen
F (単精度浮動小数点命令) 2.2 Ratified
D (倍精度浮動小数点命令) 2.2 Ratified
Q (4倍精度浮動小数点命令) 2.2 Ratified
C (16ビット圧縮命令) 2.0 Ratified
Ztso (Total-Store Orderingメモリアクセス) 0.1 Frozen
Counters (カウンタ・タイマ命令) 2.0 Draft
L (Decimal浮動小数点命令) 0.0 Draft
B (Bit-Manipulation命令) 0.0 Draft
J (JIT命令) 0.0 Draft
T (Transactional Memory) 0.0 Draft
P (Packed SIMD命令) 0.2 Draft
V (Vector命令) 0.7 Draft
N (ユーザレベル割り込み) 1.1 Draft
Zam (ミスアラインアトミック命令) 0.1 Draft

Prefaceより、変更点を要約。

  • 2019年初頭に理事会によって批准されたISAモジュールの説明を"Ratified"に移動。
  • A拡張をRatifiedから削除。
  • ISAモジュールのバージョンとの混同を避けるために文書のバージョン体系を変更。
  • 承認されたRVWMOメモリモデルの存在、および以前の基本ISAにあったFENCE.I、カウンタ、およびCSR命令の除外を反映して、基本整数ISAのバージョン番号を2.1にバージョンアップ。
  • F拡張、D拡張のバージョンを2.2にバージョンアップ。
    • バージョン2.1の正規のNaNを変更。
    • バージョン2.2がNaN Boxingスキームを定義。
    • FMINおよびFMAX命令の定義を変更。
  • ドキュメントの名前"Unprivileged"マニュアルに名称変更。
    • プラットフォームプロファイルの権限とは別のISA仕様への移行の一環
  • 実行環境(Execution Environments)、Hart、トラップ(Trap)、およびメモリアクセスのより明確で正確な定義を追加。
  • 定義済みの命令セットをカテゴライズする :
    • 標準(Standard)、予約済み(Reserved)、カスタム(Custom)、非標準(Non-standard)、不適合(Nonconforming)。
  • Big-Endian、Little Endianに関する操作を示す文章を削除。
  • ミスアラインのロードとストアの動作に関する説明を変更。
    • ユーザモードにおいて、ミスアラインのロードストアを見えないように処理する権限を与えるのではなく、実行環境のインターフェイスでミスアラインメモリアクセスのアドレス例外を参照できるようになった。これにより、エミュレートされるべきではないミスアラインのアクセス(Atomic操作を含む)に対して例外が通知されるようになった。
  • Zifencei ISAグループを定義。これにはFENCE.Iが含まれる。
    • FENCE.IはLinuxユーザABIから削除された。
    • ただし、RISC-Vではこの命令が唯一の標準命令フェッチコヒーレンスを取る命令である。
  • RV32Eを他の拡張と一緒に使用する際の禁止事項を削除。
  • RV32EおよびRV64Iの章で、特定のエンコーディングが不正命令例外を生成するというプラットフォーム固有の権限を削除。
  • カウンタ・タイマに関するCSR命令を別のグループに移動。Counter拡張Ver2.0としてマークされる。
    • カウンタ/タイマ命令は必須の基本ISAの一部とは見なされなくなったため。
    • カウンターの不正確さを含む未解決の問題があるため、Ratifiedの準備ができていない。
  • CSRアクセスOrderingモデルが追加された。
  • 2ビットのfmtフィールドで、浮動小数点命令用の16ビット半精度浮動小数点フォーマットを明示的に定義。
  • FMIN.fmtFMAX.fmtの符号付きゼロの動作を定義
    • IEEE-754-201xの提案仕様のminimumNumberとmaximumNumberの操作に準拠するように、Signaling-NaNの動作を変更。
  • メモリコンシステンシモデルRVWMOを定義。
  • "Zam"拡張を定義
    • ミスアラインなAMOを実行可能とする。
  • "Ztso"拡張を定義
    • RVWMOよりも厳密なメモリ一貫性モデルを適用する。
  • 説明と解説の改善。
  • 命令アドレスのアラインメントの制約を表すための略語としてIALIGNという用語を定義。
  • P拡張の章を削除。
  • V拡張の章を削除。

"Ratified"という記法について

Frozen, Draftに加えてRatifiedという表記に変更になったモジュールがある。Ratifiedについて、RISC-V Foundationの説明によると、

riscv.org

これはこれまでのFrozenと同じ意味だととらえて良いのだろうか。ソフトウェア開発者はRatifiedされた使用については同様のRISC-Vコアでは永久に動作させることができるとしており、Ratifiedになった仕様は変わることがないように思われる。

一方でFrozenはRatifiedの前の状態を示しており、これ以上大きく使用が変更される事は無いという意味を示している。つまり、

  • Draft(議論中) → Frozen(ほぼ決定) → Ratified(承認済み)

の手順を取るものと思われる。20190708版において現在Frozenの状態になっているのはAtomicのみであり、Atomicのみはまだ批准されていないものと思われる(これは"Zam"命令拡張の定義の追加によるものと思われる)。

RISC-V仕様書のバージョン体系を変更

これまでは

  • "The RISC-V User-Mode ISA Manual Version 2.2"

という表記だったものを、

  • "The RISC-V Instruction Set Manual. Volume I: Unprivileged ISA. Document Version 20190608-Base-Ratified"

と名称変更。これは仕様書中にバージョンの異なる複数の拡張が含まれていることを意味する。これはつまり、

  • "User-Mode"ではなく"Unprivileged ISA"に変更。"Privileged ISA"との対比と思われる。

命令セットカテゴリとは何を意味するのか

RISC-Vの命令エンコーディングおよびCSRアドレス領域に、明確なカテゴリが入るようになった。

  • Standard(標準) : RISC-V Foundationによって定義されている命令群のこと。同一のベースISAに関して、他の標準拡張命令と競合してはならない。
    • 現在のStandard拡張は"MAFDQLCBTPV"
  • Reserved(予約) : 現在命令は定義されていないが、将来の標準拡張のために予約されている。
  • Custom(カスタム) : 標準の拡張には決して使用されず、ベンダー固有の「非標準(Non-standard)」の拡張に対して有効となる。
  • 非標準「(Non-standard)」の中でも、StandardもしくはReservedのエンコーディングを使用する命令のことを「不適合(Non-conforming)」と定義する。

浮動小数点周りの仕様変更

16ビット半精度に関する命令定義が明確に追加。fmtビットフィールドに16ビット半精度の項目が追加。

fmt field Mnemonic Meaning
00 S 32-bit single-precision
01 D 64-bit double-precision
10 H 16-bit half-precision
11 Q 128-bit quad-precision

メモリアクセスに関する変更

  • IALIGNという用語を定義している。「命令アドレスアラインメント」基本的にRISC-Vの命令セットは32ビットなので32ビットであるが、C拡張が入る場合は16に緩和している。IALIGNは32と16以外の取ることは許されない。

  • RVWMOメモリコンシステンシモデルがBase ISAに追加。

  • "Zifencei"拡張命令が追加。このグループには、FENCE.I命令のみ含まれる。
  • "Zam" ミスアラインアトミック命令仕様
    • "A"拡張をさらに拡張するもの。Atomic操作をミスアラインの領域に対して実行した場合、"Zam"を実装しているプラットフォームでは、同じアドレスおよび同じサイズへの、非アトミックロードおよびストアを含む、他のアクセスを実行するだけで良い。"Zam"を実装している実行環境は、次の公理に従う。
    • ミスアラインアトミックのためのアトミック公理 :
      • rとwがHartHから発行されたミスアラインのロード・ストア命令であり、それぞれのロードストア命令が同じサイズである場合。
      • グローバルメモリオーダにおいて、Hart H以外からの同一アドレスおよび同一サイズのストア命令sは、rとwによって発行されるメモリアクセス操作の間に存在してはならない。
      • さらに、rとwと同じサイズで、グローバルメモリオーダにおいて、Hart H以外から発行されるロード命令lは、rとwによって発行されるメモリアクセス操作の間に存在してはならない。
  • "Ztso" 命令仕様を追加。Total Store Ordering