2024年07月

2024年07月12日

ずっと問題なく動いていたコードが LLVM 18 から動かなくなってしまったことをきっかけに始まった調査でしたが、気が付けばけっこうな分量になりました。

SoftFloatの未定義動作バグ(1)signedのunsignedな絶対値を求める際にINT_MIN
SoftFloatの未定義動作バグ(2)RISC-VのRV64Iではunsignedの32bit即値でも64bitレジスタの上位32bitが0とは限らない
RV64Iでunsignedの32bit値が符号拡張されないで関数にレジスタ渡しされるのはどういう時か?
SoftFloatの未定義動作バグ(3)そもそも単精度浮動小数点数演算をソフトウェアエミュレーションする関数の仮引数はfloatにするべき

そして、コンパイラの最適化のバグではなく、コードにもともとあった問題が顕在化したという結論になりました。詳細は上記リンクを参照してください。

その調査の過程で LLVM 18 の驚異的な最適化を目の当たりにしました。以下のように float を uint32_t として扱い、符号ビットを比較するコードが…
    aSign = a >> 31;
    bSign = b >> 31;
    if ( aSign != bSign ) { ... }
RV64I ターゲットでは uint32_t であっても常に 64 bit に符号拡張されるという RISC-V の呼び出し規約を利用し、(64 bit の)xor を取って符号ビット(bit 31 ではなく bit 63)が 1、つまり負の数ならば符号ビットが異なるので次にジャンプ(Branch Less Than Zero; bltz)というコードが生成されました。
  xor         a2, a1, a0
  bltz        a2, 0x1c
LLVM 17 までは、以下のように下位 32 bit を 31 bit 論理右シフト(Shift Right Logical Immediate Word; srliw)して比較(Branch Not Equal; bne)という素直なコードが出ていました。そのため問題のあるコードでも期待通り動作していたのです。
  srliw       a2, a0, 0x1f
  srliw       a3, a1, 0x1f
  bne         a2, a3, 0x38
こんな最適化、いったいどうやったら実現できるのでしょうか?調べてみました。

続きを読む

2024年07月11日

前々回の記事では
これを「未定義動作バグ」と呼ぶのが適切なのかは自信がありませんが、おそらくプログラマには 「uint32_t を 31 ビット右シフトした場合、bit 0 以外は全て 0 になり、0x0 か 0x1 のどちらかに必ずなる」という暗黙の仮定があったのではないかと思われ、そうとは限らないという意味で未定義動作バグとしました。
前回の記事では
コンパイララインタイム関数は C 言語仕様の範疇ではなく、完全にコンパイラの実装と結びついたもの(コード生成の一部と考えられる)なので、なんでもありなのでしょう。
ではなぜ compiler-rt の関数は大丈夫なのか?という疑問が生まれますが、どうもレジスタ渡しされてきた値を一度スタックに書き込んで符号付きに変換して扱っているから大丈夫なようです。

そもそも本来は __ltsf2 に渡ってくるのは float 型のようで、そのまま uint32_t で扱ってはいけないのかもしれません。
などとごまかして終わらせてしまいましたが、ちゃんと調べました。

続きを読む

2024年07月10日

前回の記事「SoftFloatの未定義動作バグ(2)RISC-VのRV64Iではunsignedの32bit即値でも64bitレジスタの上位32bitが0とは限らない」の補足です。

RISCV RV64I では unsigned な 32 bit 値(uint32_t)であっても、常に汎用レジスタ上では符号拡張された形で扱われます。例えば f(uint32_t) 関数に f(0xc0000000) を渡した場合、第一引数が渡る a0(x10)レジスタには 0x00000000c0000000 ではなく 0xffffffffc0000000 が渡ります。Clang コンパイラもこの仕様を前提とした最適化を行っています。

ならば、符号拡張されない形で 32 bit 値が関数にレジスタ渡しされてきたならば、それはバグではないか?という疑問が生まれます。そもそもこの値はどこから来たのでしょうか?調べてみました。

続きを読む

2024年07月05日

前回の記事「SoftFloatの未定義動作バグ(1)signedのunsignedな絶対値を求める際にINT_MIN」の続きです。

前回の記事では肝心なことを書き忘れたのですが、libc の「fp ソフトウェアエミュレーション」とは、つまり libc でコンパイラランタイムライブラリ(GCC の libgcc や LLVM の compiler-rt)の関数を実装しているということです。FPU が無いターゲットでは、コンパイラは fp 命令の代わりにこの関数を呼び出します。

なぜこんな面倒なことをしているかというと、libgcc も compiler-rt も fenv.h を考慮せず、常に FE_TONEAREST で fp 演算を行うからだと思われます。現実的には FPU が無い CPU で丸め方式の考慮が必要になるようなプログラムを実行するケースは稀だと思いますが、NetBSD にはこだわりがあるのでしょう。(弊社もできる限り全てのサポートターゲットが標準に準拠した動作を行うコンパイラツールチェーン製品を目指しています。)

続きを読む

2024年07月04日

NetBSD の libc は FPU を持たないターゲット向けの fp(浮動小数点数)ソフトウェアエミュレーションに John R. Hauser 氏が開発している Berkeley SoftFloat のバージョン 2a を使用しています。
http://www.jhauser.us/arithmetic/SoftFloat.html

弊社のコンパイラツールチェーン製品 exeClang は NetBSD の libc を使用しているのですが、LLVM 18 版の開発中に RISC-V の RV64 IMA(noFPU)ターゲットをいつものように商用コンパイラテストスイートにかけると、新規 fail が 1 つ発生しました。

その原因を追究して行く過程で、この SoftFloat の未定義動作(Undefined Behavior; UB)を 2 つ発見したので紹介します。今回はそのうち 1 つ目です。

続きを読む

記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード