1. ホーム
  2. c++

[解決済み] この浮動小数点演算の最適化は許されますか?

2023-04-18 07:30:27

質問

私は float が大きな整数を正確に表現する能力を失っているところを調べてみました。そこで、こんな小さなスニペットを書いてみました。

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

このコードはclangを除くすべてのコンパイラで動作するようです。Clangは単純な無限ループを生成します。 ゴッドボルト .

これは許可されているのでしょうか?もしそうなら、それはQoIの問題ですか?

どのように解決するのですか?

Angew が指摘したように は、その != 演算子は両側で同じ型が必要です。 (float)i != i は右辺を同様に float に昇格させるので、次のようになります。 (float)i != (float)i .


g++も無限ループを生成しますが、その内部からの作業を最適化することはありません。 int->floatを変換しているのがわかると思います。 cvtsi2ss で変換し ucomiss xmm0,xmm0 を比較するために (float)i を自分自身と比較します。 (これが、@Angew の回答が説明するように、あなたの C++ ソースがあなたが考えていたような意味ではないという最初の手がかりでした)。

x != x が真であるのは、それが "unordered"であるときだけだからです。 x はNaNだったからです。 ( INFINITY は IEEE の数学でそれ自身と等しく比較されますが、NaN はそうではありません。 NAN == NAN は偽です。 NAN != NAN は真)。

gcc7.4以前は、コードを正しく最適化して jnp をループブランチとして最適化します ( https://godbolt.org/z/fyOhW1 ) : のオペランドがある限りループし続ける。 x != x へのオペランドがNaNでない限り、ループを続ける。 (gcc8以降では je をチェックし、NaN でない入力に対しては常に真になるという事実に基づいて最適化することに失敗しました)。


ちなみに、つまり clangの最適化も安全です。 : それはただCSEする必要があるだけです。 (float)i != (implicit conversion to float)i が同じであることを証明し、さらに i -> float の取り得る範囲では決してNaNにならないことを証明する。 int .

(このループが符号ありオーバーフローのUBにぶつかることを考えると、 文字通りどんなasmでも出すことが許されるのですが、その中には ud2 不正な命令や、ループ本体が実際に何であったかに関係なく空の無限ループなど、文字通り好きなasmを出すことができます)。 しかし、符号付きオーバーフロー UB を無視しても、この最適化は 100% 合法です。


GCCはループ本体を取り除く最適化に失敗する であっても -fwrapv を使って符号付き整数のオーバーフローをうまく定義しています。 (2の補数ラップアラウンドとして)。 https://godbolt.org/z/t9A8t_

を有効にしても -fno-trapping-math を有効にしても役に立ちません。 (GCCのデフォルトは 残念ながら を有効にするために

-ftrapping-math でも GCCの実装は壊れている/バグがある int->float 変換は FP の不正確な例外 (正確に表現できないほど大きな数) を引き起こす可能性があるので、例外がマスクされない可能性がある場合は、ループ本体を最適化しないことが合理的です。 (なぜなら 16777217 を float に変換すると、不正確な例外がマスクされない場合、観察可能な副作用を持つ可能性があるため)。

しかし -O3 -fwrapv -fno-trapping-math では、これを空の無限ループにコンパイルしないのは、100%ミス最適化です。 また #pragma STDC FENV_ACCESS ON がなければ、マスクされた FP 例外を記録するスティッキーフラグの状態は、コードの観察可能な副作用ではありません。 いいえ int -> float の変換はNaNになる可能性があるので x != x は真になり得ない。


これらのコンパイラはすべて、IEEE 754 単精度 (binary32) を使用する C++ 実装に最適化されています。 float と 32 ビット int .

バグフィックス (int)(float)i != i ループは、16 ビットの狭い C++ 実装では UB になっていました。 int と広い float として正確に表現できない最初の整数に到達する前に、符号付き整数のオーバーフロー UB を起こしてしまうからです。 float .

しかし、x86-64 System V ABI を持つ gcc や clang のような実装のためにコンパイルする場合、実装で定義された異なる選択肢の下で UB は何のマイナス効果もありません。


ところで、このループの結果を静的に計算することができるのは FLT_RADIXFLT_MANT_DIG で定義されています。 <climits> . 少なくとも理論的には、もし float がPosit / unumのような他の種類の実数表現ではなく、IEEE floatのモデルに実際に適合している場合、少なくとも理論的には可能です。

ISO C++ 標準規格がどの程度 float の動作についてどの程度規定しているのか、また、固定幅の指数およびシグニフィカンドのフィールドに基づかないフォーマットが標準に準拠するのかどうかについては、よくわかりません。


コメントで

<ブロッククオート <ブロッククオート

@geza 結果の数字が気になりますね~。

nada: 16777216です。

このループをprint / returnさせたと言うことでしょうか? 16777216 ?

更新:そのコメントは削除されたので、私はそうではないと思います。 おそらく、OPは単に float の前に、32 ビットで正確に表現できない最初の整数の float . https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values つまり、このバグだらけのコードで何を検証しようとしたのか。

バグフィックス版では、もちろん 16777217 である最初の整数は ではなく が正確に表現できる最初の整数であり、その前の値ではありません。

(すべての高い浮動小数点値は正確な整数ですが、それらはシグニフィカンドの幅よりも高い指数値に対して、2の倍数、次に4、次に8などとなっています。 多くの高い整数値は表現できますが、(シグニフィカンドの)最後の場所の1単位は1より大きいので、連続した整数ではありません。 最大の有限の float は 2^128 のすぐ下で、これは大きすぎて int64_t .)

もし、どのコンパイラも元のループを終了してそれを表示したとしたら、それはコンパイラのバグでしょう。