ぐだぐだ低レベルプログラミング(48) RISC-V、浮動小数点例外フラグたててみる

Joseph Halfmoon

今回も前回同様にRISC-Vの単精度浮動小数点除算命令を使います。今回は実際に計算をした結果として浮動小数点例外の5つのフラグを立ててみたいと思います。所望の例外を所望の場所(というかタイミングというか)で起こすというのは難しいというかメンドイです。そのくせ、起きてほしくないときに起きるんだけれども。例外ってやつは。

※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら

まず、アイキャッチ画像に掲げさせていただきましたチップは、今回も「テスト・ビークル」としてテストコードを走らせております、64ビットRISC-Vコアを2個登載したKendryte K210であります。

浮動小数点例外フラグへのアクセス

浮動小数点例外フラグは、第45回で説明した fcsr レジスタの下5ビットに格納されています。多分、頻繁にやりたくなる例外フラグのクリア操作を、fcsr レジスタの直接書き換えで行おうとすると、無関係な丸め制御ビットを書き換えないようにリードモディファイライトしないとなりません。そのためか、浮動小数点例外フラグ単独でアクセスできるようにcsr上に別アドレスが用意されています。さらに、「メンドイ」csrへのアクセス命令でなく、専用の疑似命令が用意されています。

frflags rd

浮動小数点例外フラグを整数レジスタrdへ読み出します

fsflags rd, rs1

浮動小数点例外フラグに整数レジスタrs1の内容を書き込み、書き込み前の浮動小数点例外フラグを整数レジスタrdへ読み出します

上記の疑似命令を使えば、浮動小数点例外フラグの5ビットを読み出すことが可能です。が、その実行タイミングは結構微妙です。以前にもちっと確認した様に浮動小数点演算命令は演算パイプラインに入ってから出るまでが長いです。手元でやってみたところでは、浮動小数点演算命令を発行した直後に浮動小数点例外フラグを読み出してもまだ反映されていない、ということがあるみたいでした。そこで、今回は「とりあえず」浮動小数点演算の結果を整数レジスタに転送する命令を発行した後で(演算結果自体は演算完了してから読み出せているみたいなので)浮動小数点例外フラグを読み出してます。しかし、後で述べるように、そのタイミングでもまだ早いのではないか、という事例もあり。どうやって読んだら確実なのかは要調査であります。これはRISC-VというよりK210の実装に関することじゃないかと思います。

被テスト関数

被テスト関数のソースコードを以下に掲げました。手順は以下のようです。

    1. 除算の引数は、浮動小数点フォーマットの符号無整数として整数レジスタ経由で与える
    2. 除算命令実行直前に、fflagsの値を読み出した上で、fflagsをゼロクリアする
    3. 単精度浮動小数点除算を行う
    4. 除算命令実行後、除算の実行結果を整数レジスタに読みだす
    5. fflagsの値も読み出す
    6. 演算結果がCの float型のMAX, MINの範囲かどうか確認(面倒なので、今回は全て正の数と0で計算してしまいました。負の数はなし。手抜きだな、自分。)
    7. 結果を標準出力に印字
void tst_FdivFFlag(uint32_t tnum, uint32_t arg1, uint32_t arg2)
{
  uint32_t fflag0 = 0;
  uint32_t fflag1 = 0;
  uint32_t fflag2 = 0;
  TestFloat f0S, f1S, f2S;

  f0S.fDat = 0.0;
  f1S.u32Dat.LowW = arg1;
  f2S.u32Dat.LowW = arg2;
  asm volatile("fmv.w.x f1, %[Rs1]\n\t"
               "fmv.w.x f2, %[Rs2]\n\t"
               "fsflags %[Rd0], %[Rd1]\n\t"
               "fdiv.s f0, f1, f2\n\t"
               "fmv.x.w %[Rd], f0\n\t"
               "frflags %[Rd2]\n\t"
              : [Rd] "=r" (f0S.u32Dat.LowW), [Rd0] "=r" (fflag0), [Rd1] "=r" (fflag1), [Rd2] "=r" (fflag2)
              : [Rs1] "r" (f1S.u32Dat.LowW), [Rs2] "r" (f2S.u32Dat.LowW)
              : );
  Serial.printf("TEST #%u\r\n", tnum);
  if (f0S.fDat > FLT_MAX) {
    Serial.printf("OVER FLT_MAX\r\n");
  }
  if (f0S.fDat < FLT_MIN) {
    Serial.printf("UNDER FLT_MIN\r\n");
  }
  Serial.printf("%f / %f = %f\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("%e / %e = %e\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("FFlags: %08x %08x %08x\r\n", fflag0, fflag1, fflag2);
  Serial.printf("  HEX: %08x\r\n", f0S.u32Dat.LowW);
  Serial.printf("\r\n");
}
テスト項目

テスト項目は以下の8つです。

    1. 割り切れる計算。
    2. 割り切れない(丸めが起こる)計算
    3. 正の無限大を正の無限大で割る
    4. 非ゼロの普通の数をゼロで割る
    5. アンダーフローが発生する筈の割り算
    6. オーバーフローが発生する筈の割り算
    7. NaN(Not a Number)を普通の数で割る
    8. 普通の数をNaNで割る

メインルーチン内の被テスト関数呼び出し部分が以下に。

#define   F_NaN   0x7FC00000
#define   F_INFP  0x7F800000
#define   F_INFN  0xFF800000
#define   F_DAT1  0x3F000000
#define   F_DAT2  0x40000000
#define   F_DAT3  0x40400000
#define   F_DAT4  0x3E000000
#define   F_DAT5  0x7EFFFFFF
#define   F_ZERO  0x00000000

//~途中略~
  tst_FdivFFlag(1, F_DAT1, F_DAT2);
  tst_FdivFFlag(2, F_DAT2, F_DAT3);

  tst_FdivFFlag(3, F_INFP, F_INFP);
  tst_FdivFFlag(4, F_DAT1, F_ZERO);

  tst_FdivFFlag(5, F_DAT1, F_DAT5);
  tst_FdivFFlag(6, F_DAT5, F_DAT4);

  tst_FdivFFlag(7, F_NaN , F_DAT1);
  tst_FdivFFlag(8, F_DAT1, F_NaN );
実行結果

まずはTEST1「割り切れる」計算と、TEST2「割り切れない」計算の結果から。こんな感じ。

TEST #1
0.500000 / 2.000000 = 0.250000
5.000000e-01 / 2.000000e+00 = 2.500000e-01
FFlags: 00000010 00000000 00000000
HEX: 3e800000

TEST #2
2.000000 / 3.000000 = 0.666667
2.000000e+00 / 3.000000e+00 = 6.666667e-01
FFlags: 00000000 00000000 00000001 <=== NX Flag
HEX: 3f2aaaab

割り切れる計算では、フラグはたちませんが、割り切れない計算ではNX Flagが立っていることが分かります。NX、Inexact。演算結果が「不正確」なことを示すフラグです。予定通りとな。

次にTEST3「無限大同士の割り算」と、TEST4「ゼロ割り算」です。

TEST #3
inf / inf = nan
inf / inf = nan
FFlags: 00000001 00000000 00000010 <=== NV Flag
HEX: 7fc00000

TEST #4
OVER FLT_MAX
0.500000 / 0.000000 = inf
5.000000e-01 / 0.000000e+00 = inf
FFlags: 00000010 00000000 00000008 <=== DZ Flag
HEX: 7f800000

無限大同士の割り算の結果は NaNとなり、NV、無効演算フラグが立っています。一方普通の数をゼロで割った場合、結果は無限大となり、DZフラグ、「有限でゼロでない数に対するゼロによる割り算フラグ」が立っています。勿論、このときは、Cの標準ヘッダ、float.hで規定されるFloat MAXを超えています。

続いてTEST5「アンダーフローが発生する割り算」と、TEST6「オーバーフローが発生する割り算」です。

TEST #5
UNDER FLT_MIN
0.500000 / 170141173319264429905852091742258462720.000000 = 0.000000
5.000000e-01 / 1.701412e+38 = 2.938736e-39
FFlags: 00000008 00000000 00000003 <=== UF NX Flag
HEX: 00200000

TEST #6
OVER FLT_MAX
170141173319264429905852091742258462720.000000 / 0.125000 = inf
1.701412e+38 / 1.250000e-01 = inf
FFlags: 00000003 00000000 00000005 <=== OF NX Flag
HEX: 7f800000

この2つの挙動は微妙に異なります。アンダーフローの方は、UFフラグ、「表現可能な最小の正規化数(ノーマル数)を下回った」とNXフラグの両方が立っていますが、それでも割り算の結果自体は得られています。デノーマル数(非正規化数)というやつ。標準Cのヘッダの下限は正規化数の下限の筈なので、それ下回っていますが、曲りなりでも結果は出とる、と。

一方オーバーフローの方は、OFフラグ、NXフラグの両方が立った上で、結果は無限大になってます。ビット表現の余地がないのでこうなるかと。「OF オーバーフロー。浮動小数点フォーマット上表現可能な最大数を超えた」であります。

続いてTEST7「NaNを割る」計算と、TEST8「NaNで割る」計算です。

TEST #7
nan / 0.500000 = nan
nan / 5.000000e-01 = nan
FFlags: 00000005 00000000 00000000
HEX: 7fc00000

TEST #8
0.500000 / nan = nan
5.000000e-01 / nan = nan
FFlags: 00000010 00000000 00000000
HEX: 7fc00000

NaNを割っても、NaNで割っても、結果はNaNです。しかし、現在のfflagsの観察タイミングだと例外フラグは立っていないように見えます。しかし、後からみると、NVフラグが立っているような形跡があり。NVフラグのセットは、大分遅れているのでしょうかね? この点については別途、もう少し実機で調べたいと思います。

やっぱり、浮動小数点演算メンドイですな。

ぐだぐだ低レベルプログラミング(47) RISC-V、K210、浮動小数点除算サイクル数 へ戻る

ぐだぐだ低レベルプログラミング(49) RISC-V、アリガチな浮動小数点加減算の落とし穴 へ進む