RISC-Vのアーキテクチャでは、浮動小数点を扱うレジスタは整数レジスタとは別に用意されている。 基本的にFloat / Double / Quad も1種類の浮動小数点レジスタを利用して格納されるので、ハードウェア的にレジスタ長がどのくらいの大きさを持っているのかについては、FLENという値でもって定義されている。 FLEN=32の場合は単精度を格納することができ、FLEN=64の場合は単精度と倍精度、FLEN=128の場合は単精度、倍精度、4倍精度までデータを格納することができる。
このとき、注意しなければならないのは、単精度の命令であろうが倍精度の命令であろうが、オペランドは"f"レジスタという名前で指定する。 識別可能なのは、オペコードの表記であるs / dだけである。 つまり、基本的に単精度浮動小数点命令であろうと、倍精度浮動小数点命令であろうと、指定されるレジスタオペランドは同一のものである。
fadd.s ft1,ft0,ft0 // 単精度 fadd.d ft1,ft0,ft0 // 倍精度
このあたりのテストパタンを調査していて気がついたのだが、RISC-Vの浮動小数点命令では、各浮動小数点命令の精度が異なるなかでそれらをうまく共有するために"NaN Boxing"という手法が使われている。 私は初めて知った手法なのだが、どうやら一般的な手法なようだ。
FLEN=64で単精度浮動小数点のデータ操作をする場合
例えば、FLEN=64つまり浮動小数点レジスタの長さが64ビットであった場合、単精度と倍精度の両方が格納されるわけだが、格納されているデータが単精度である、倍精度であるということを区別するためにはどのようにすればよいのであろうか。
この時に使われる N-Boxingという手法であるが、例えば、FLEN=64の中でFLEN=32の単精度のデータを格納する場合、上位の32ビットをすべて1で埋めてしまう。
以下 RISC-V ISA Specification V2.2 より抜粋。
When multiple floating-point precisions are supported, then valid values of narrower n-bit types, n < FLEN, are represented in the lower n bits of an FLEN-bit NaN value, in a process termed NaN-boxing. The upper bits of a valid NaN-boxed value must be all 1s. Valid NaN-boxed n-bit values therefore appear as negative quiet NaNs (qNaNs) when viewed as any wider m-bit value, n < m ≤ FLEN.
例えば、FLEN=64で単精度浮動小数点の1.0を表現する場合は、以下のようにして、上位の32ビットを埋めてしまう。これがNaN Boxingだ。
FFFFFFFF_3F800000 // 1.0
これ、何がNaNなのかというと、FLEN=64である倍精度浮動小数点からしてみるとこの値はNaNとして認識される。 つまり、上位がすべて1ならば、本来表現したい精度の浮動小数点よりも大きな精度の浮動小数点では、常にNaNとなる。
3F800000 // 1.0 FFFFFFFF_3F800000 // 単精度では1.0 倍精度ではNaN FFFFFFFF_FFFFFFFF_FFFFFFFF_3F800000 // 単精度では1.0、倍精度ではNaN、4倍精度ではNaN
これにより、レジスタに付加情報を持たせることなく、どの精度の値が格納されているかということを検証することができる。 これがNaN Boxingの基本的な考え方だ。
RISC-V における NaN-Boxingの利用
さて、これらの技法は、RISC-Vではどのようにして使われているのだろうか? (というか実際にはテストパタンでNaN Boxingが発生して、何故だろう?と調査した結果NaN Boxingについて初めて知ったわけだが...)
- 浮動小数点数移動命令 FMVx : 浮動小数点数移動命令FMVでは、FLENよりも小さな値を移動するとき、その値はNaN Boxingされる。
- 浮動小数点符号挿入命令 FSGNJx : RISC-Vには浮動小数点の符号を扱うための命令がある (だいたいこれらはFABS やFNEGのベースの命令として使用される)。これらの命令も、扱うデータがFLENよりも小さい場合 (例えば、FSGNJ.S 命令をFLEN=64の環境で実行する場合)、入力値がNaN-Boxingされているかどうか (つまり上位の32ビットがすべて1になっているか)をチェックし、レジスタに書き込まれる値も常にNaN-Boxingされている
RISC-V における Nan-Boxingの実装
逆に、これらのソースコードを追いかけている間に、NaN-Boxingについて知ったわけだが...
riscv-isa-sim の場合
例えば fsgnj.s
命令の場合には、以下のような実装となっている。
- riscv-isa-sim/riscv/insns/fsgnj_s.h
require_extension('F'); require_fp; WRITE_FRD(fsgnj32(FRS1, FRS2, false, false));
- riscv-isa-sim/riscv/decode.h
/* Convenience wrappers to simplify softfloat code sequences */ #define isBoxedF32(r) (isBoxedF64(r) && ((uint32_t)((r.v[0] >> 32) + 1) == 0)) #define unboxF32(r) (isBoxedF32(r) ? (uint32_t)r.v[0] : defaultNaNF32UI) #define isBoxedF64(r) ((r.v[1] + 1) == 0) #define unboxF64(r) (isBoxedF64(r) ? r.v[0] : defaultNaNF64UI) typedef float128_t freg_t; inline float32_t f32(freg_t r) { return f32(unboxF32(r)); } inline float64_t f64(freg_t r) { return f64(unboxF64(r)); }
Rocket-Chipの場合 (Chisel)
Rocket-Chipの場合も、入力値に対してNaN-Boxingを適用する論理が挿入されている。
val fsgnjMux = Wire(new FPResult) fsgnjMux.exc := UInt(0) fsgnjMux.data := fsgnj when (in.bits.wflags) { // fmin/fmax val isnan1 = maxType.isNaN(in.bits.in1) val isnan2 = maxType.isNaN(in.bits.in2) val isInvalid = maxType.isSNaN(in.bits.in1) || maxType.isSNaN(in.bits.in2) val isNaNOut = isnan1 && isnan2 val isLHS = isnan2 || in.bits.rm(0) =/= io.lt && !isnan1 fsgnjMux.exc := isInvalid << 4 fsgnjMux.data := Mux(isNaNOut, maxType.qNaN, Mux(isLHS, in.bits.in1, in.bits.in2)) }
というわけで、例えばfsgnj命令の実装する場合も、単純に上位の符号のみ書き換えればよいわけではないので注意。