はじめに
結論から書きます。RISC-Vでは(そして一般的にも?)1本の、例えば128bit精度の浮動小数点レジスタに32bit, 64bitの浮動小数点値を書く時には、下位にその値をおいて、利用していない上位を1で埋める作業を行います。これをNaN Boxingと呼びます。NaN Boxingでは、使っていない上位がすべて1で埋まってなくてはなりません。もし1でうまってない数があった場合は、いかに下位が正しく32bit/64bitの浮動小数点値を示していても、それはNaNとして扱われます。
問題の出発点 - fsgnj.s命令が想定と違う動きをする
RISC-VのISS(instruction set simulator)を自作している中で、fsgnj.s命令のテストがどうしてもパスせず困っていました。fsgnj.s f0, f1, f2命令はsign injection命令で、f1のsign bitだけf2のsign bitと入れ替えてf0に格納する、という命令です。精度はfloat(32bit)です。
これのテストがriscv-testsの以下ファイルの58-61行目にあります。
これの58行目のテスト、テスト番号40のテストにおいて、fsgnj.sを使っている箇所はマクロの48行目になります。以下、spikeにてステップ実行した際の上記48行目のfsgnj.s命令実行直後におけるレジスタの値のダンプです。
core 0: 0x0000000080000608 (0x20208053) fsgnj.s ft0, ft1, ft2
: freg 0 ft0
0xffffffffffffffffffffffff7fc00000
: freg 0 ft1
0xffffffffffffffff7ffffffe12345678
: freg 0 ft2
0xffffffffffffffff0000000000000000
レジスタ自体は128bitあるようですが、この命令では単精度、32bitしか使われないため、下位32bitだけを見ていました。そうすると、ft1は0x12345678, ft2は0x00000000、どちらも浮動小数点の値として正しい値(NaNではない、という意味)です。ft2のサインビットは0のため、ft1のサインビットを0に書き換えて(も元のママ)0x12345678がft0に入るはずだ、と思っていました。ところが、spikeの実行結果では、0x7fc00000がft0に入っていました。Canonical NaNというやつらしいです(quiet NaNでもある)。どうしてNaNになるのか全くわからず、NaNなんだ!NaNでなんだ!とうめいてrisc-vのslackサーバーに質問を投げつけました。
問題のポイント - NaN Boxingは上位が全てall 1で埋まってないとNaNとみなす
NaN Boxingとは、前述したように、利用していない上位bitを1で埋める作業を指します。NaNは、exponentがall 1で、fractionが0以外の場合を指します。上位を1で埋めると、自然とexponentとfractionの上位何ビットかは必ずall 1になります。つまり、32bit幅で浮動小数点レジスタに書き込んだ値は、64bit/128bitで読もうとするとNaNに見えてしまうわけです。詳細はriscvの仕様書p73-74に記載されてあります。ここで、ポイントは以下のパラグラフです
Apart from transfer operations described in the previous paragraph, all other floating-point operations on narrower n-bit operations, n < FLEN, check if the input operands are correctly NaN-boxed, i.e., all upper FLEN−n bits are 1. If so, the n least-significant bits of the input are used as the input value, otherwise the input value is treated as an n-bit canonical NaN.
ここで、FLENとは浮動小数点レジスタの幅で、上記のデフォルトのspikeだと128のようです。これを読むと、input operandは正しくNaN Boxingされているか、つまり、利用されていない上位bitがすべて1かを確認する必要があります。そして、もし正しくNaN Boxingされていない場合は、いくら下位の値が正しく浮動小数点の値を示していても、canonical NaNとして扱え、とあります。Oh, MY! ft1レジスタは上位bitに1ではないものが含まれています。そのため、ft1はfloat精度で読み取った時にもNaNとして、つまり0x7fc00000として扱わなければなりません。0x7fc00000にsign bitをinjectionした結果は0x7fc00000です。このように、spikeの実行結果の説明がつきました。
教訓
仕様書は舐めるように端から端まで読みましょう(難しい!
最後に
そして検索したらFPGA開発日記さんにて同じ問題が指摘されていました。ああ、NaN Boxingという言葉でぐぐる能力があれば...