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

Joseph Halfmoon

前回までに浮動小数点の例外とか丸めとかのメンドイ部分を調べました。今回からは実際に計算していきたいと思います。まずは浮動小数の加算と減算からですな。ただ足し算が出来たと喜ぶのでは芸が無いので、皆さんご存じな「駄目」ケースの計算をあえてやって、バイナリで結果を見て行きたいと思います。

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

今回の実験は、RISC-Vの単精度浮動小数点命令を使ってはいますが、RISC-Vのせいではなく、また、「ビーグル」として使っているKendryte K210のせいでもありません。普通の浮動小数点演算をしている限り逃れられないものでございます。思い起こせば40年以上昔の遥かな「古代」、大学初年級の選択科目のFortran(多分当時は全部大文字)の授業の最初の方で教わった(記憶は朧気です)事であります。

    1. 「似たような」大きさの数どうしを引き算してはいけない(正負を考えれば似たような絶対値の数を足し算してもいけない)
    2. 大きな数に小さな数を足す(引く)ときも注意

最初のやつはいわゆる「桁落ち」、2番目のやつは「情報落ち」ってやつですね。計算機を使ってプログラムしていたら避けられないものども。今回は、それをRISC-Vのインラインアセンブラで記述した関数で観察して行きたいと思います。

桁落ちテスト用の引き算ルーチン

以下に被テスト関数のソースを掲げましたが。単なる単精度浮動小数点減算命令1個です。3オペランドなので、以下の例ではf1からf2を引いてf0を求めています。関数の引数の2番目がf1、3番目がf2に入るので、ここに桁落ちするような数の組み合わせを入れて呼べば桁落ちするところが観察できる筈。よゐこは、当然ながらそういう数の組み合わせにならないように配慮してプログラムする、というきまりですが。こんな感じ。

void tst_Fsub(uint32_t tnum, float arg1, float arg2)
{
  TestFloat f0S, f1S, f2S;

  f0S.fDat = 0.0;
  f1S.fDat = arg1;
  f2S.fDat = arg2;
  asm volatile("fmv.w.x f1, %[Rs1]\n\t"
               "fmv.w.x f2, %[Rs2]\n\t"
               "fsub.s f0, f1, f2\n\t"
               "fmv.x.w %[Rd], f0\n\t"
              : [Rd] "=r" (f0S.u32Dat.LowW) 
              : [Rs1] "r" (f1S.u32Dat.LowW), [Rs2] "r" (f2S.u32Dat.LowW)
              : );
  Serial.printf("TEST-fsub #%u\r\n", tnum);
  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("  HEX: %08x\r\n", f0S.u32Dat.LowW);
  Serial.printf("\r\n");
}
情報落ちテスト用の足し算ルーチン

以下に被テスト関数を2つ並べてあります。

    • tst_FaddMulti0 「完璧な情報落ちが期待できる」コード
    • tst_FaddMulti1 「多少の情報落ちで済むはずの」コード

どちらも、小さな数を多数回積算していくことをエミュレートするために、第3引数の浮動小数を第4引数の符号無整数の回数だけ足しこんでいます。ただし、足し込み方が最初の0と後の1では違っています。0の方は、積算するレジスタに初期値が立てられるようになっており、そこに足し込んでいきます。1の方は、積算レジスタの初期値は0で、そこに足し込んだ後、最後に初期値に積算結果を加えて答えを求めています。

void tst_FaddMulti0(uint32_t tnum, float arg1, float arg2, uint32_t arg3)
{
  TestFloat f0S, f1S, f2S;

  f0S.fDat = 0.0;
  f1S.fDat = arg1;
  f2S.fDat = arg2;
  asm volatile("fmv.w.x f1, %[Rs1]\n\t"
               "fmv.w.x f2, %[Rs2]\n\t"
               "fmv.w.x f0, %[Rd]\n\t"
               "mv t0, %[RL]\n\t"
               "LBL_fam0:\n\t"
               "fadd.s f1, f1, f2\n\t"
               "addi t0, t0, -1\n\t"
               "bgtz t0, LBL_fam0\n\t"
               "fmv.x.w %[Rd], f1\n\t"
              : [Rd] "=r" (f0S.u32Dat.LowW)
              : [Rs1] "r" (f1S.u32Dat.LowW), [Rs2] "r" (f2S.u32Dat.LowW), [RL] "r" (arg3)
              : "t0");
  Serial.printf("TEST-fadd.s Multi0 #%u\r\n", tnum);
  Serial.printf("%f + sum(%f) = %f\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("%e + sum(%e) = %e\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("  HEX: %08x\r\n", f0S.u32Dat.LowW);
  Serial.printf("\r\n");
}

void tst_FaddMulti1(uint32_t tnum, float arg1, float arg2, uint32_t arg3)
{
  TestFloat f0S, f1S, f2S;

  f0S.fDat = 0.0;
  f1S.fDat = arg1;
  f2S.fDat = arg2;
  asm volatile("fmv.w.x f1, %[Rs1]\n\t"
               "fmv.w.x f2, %[Rs2]\n\t"
               "fmv.w.x f0, %[Rd]\n\t"
               "mv t0, %[RL]\n\t"
               "LBL_fam1:\n\t"
               "fadd.s f0, f0, f2\n\t"
               "addi t0, t0, -1\n\t"
               "bgtz t0, LBL_fam1\n\t"
               "fadd.s f1, f1, f0\n\t"
               "fmv.x.w %[Rd], f1\n\t"
              : [Rd] "=r" (f0S.u32Dat.LowW)
              : [Rs1] "r" (f1S.u32Dat.LowW), [Rs2] "r" (f2S.u32Dat.LowW), [RL] "r" (arg3)
              : "t0");
  Serial.printf("TEST-fadd.s Multi1 #%u\r\n", tnum);
  Serial.printf("%f + sum(%f) = %f\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("%e + sum(%e) = %e\r\n", f1S.fDat, f2S.fDat, f0S.fDat);
  Serial.printf("  HEX: %08x\r\n", f0S.u32Dat.LowW);
  Serial.printf("\r\n");
}
被テスト関数の呼び出し

被テスト関数の呼び出しコードは以下のようです。

tst_Fsub(1, 7.654321e-5, 1.234567e-5);
tst_Fsub(2, 7.654321e-5, 7.654311e-5);

tst_FaddMulti0(1, 1.15, 1.3e-8, 1000);
tst_FaddMulti1(2, 1.15, 1.3e-8, 1000);
実機上での実験結果

TEST-fsub #1は、桁落ちするような組み合わせでないです。指数表記の印字を見ても分かりますが、最後のHEX表示の結果を見ても仮数部にミッチリ数字が詰まっていることが見てとれます。

TEST-fsub #2は、桁落ちするような例です。10進指数表記の印字をみると何やら沢山の桁が見えます。しかし、与えている10進引数同士の引き算の結果(暗算簡単)を考えると、約2%も大きな値が表示されています。10進表現に騙されてはいけませぬ。最後のHEX表示をみると、仮数の下の方はゼロがつまっていて有効数字の桁が落ちていることがハッキリわかります。

TEST-fadd.s Multi0 #1は、大きな初期値に小さな値を1000回足してますが、小さな数の足し込みの努力がまるで無視されています。

TEST-fadd.s Multi0 #2は、小さな値を1000回足した後で、大きな初期値に加えています。小さい値の積算値もそれなりに反映されている(まあ「作った値」なので当然ちゃ当然ですが)のが分かります。

Trial: 4
TEST-fsub #1
0.000077 - 0.000012 = 0.000064
7.654321e-05 - 1.234567e-05 = 6.419754e-05
HEX: 3886a1cb

TEST-fsub #2
0.000077 - 0.000077 = 0.000000
7.654321e-05 - 7.654311e-05 = 1.018634e-10
HEX: 2ee00000

TEST-fadd.s Multi0 #1
1.150000 + sum(0.000000) = 1.150000
1.150000e+00 + sum(1.300000e-08) = 1.150000e+00
HEX: 3f933333

TEST-fadd.s Multi1 #2
1.150000 + sum(0.000000) = 1.150013
1.150000e+00 + sum(1.300000e-08) = 1.150013e+00
HEX: 3f9333a0

今回は、RISC-Vの命令は使いましたが、一般的な話で終わりました。次回はも少しRISC-Vよりに行きたいです。大丈夫か。

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

ぐだぐだ低レベルプログラミング(50) RISC-V、浮動小数積和演算、速さだけでないノダ へ進む