web-dev-qa-db-ja.com

この浮動小数点最適化は許可されていますか?

floatが大きな整数を正確に表す機能を失う場所を調べてみました。だから私はこの小さなスニペットを書きました:

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

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

これは許可されますか?はいの場合、それはQoIの問題ですか?

88
geza

@ Angewが指摘したように!=演算子は両側で同じ型が必要です。 (float)i != iを指定すると、RHSも浮動に昇格するため、(float)i != (float)iになります。


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

x != xは、xがNaNであるため、「順序付けなし」の場合にのみtrueです。 (INFINITYは、IEEE数学ではそれ自体と同じですが、NaNはそうではありません。NAN == NANはfalse、NAN != NANはtrueです)。

gcc7.4以前では、コードをループ分岐としてjnpに正しく最適化します( https://godbolt.org/z/fyOhW1 ):x != xのオペランドである限りループを継続しますNaNではなかった。 (gcc8以降では、jeもチェックしてループから抜け出し、NaN以外の入力に対して常にtrueであるという事実に基づいて最適化に失敗しています)。 x86 FPは、順不同でセットPFを比較します。


そしてBTWは、clangの最適化も安全であることを意味します:同じであると(float)i != (implicit conversion to float)iをCSEする必要があり、i -> floatがNaNにならないことを証明するintの範囲。

(このループは符号付きオーバーフローUBにヒットすることを前提としていますが、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をtrueにすることはできません。


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

バグ修正された(int)(float)i != iループは、狭い16ビットintおよび/またはより広いfloatのC++実装でUBを持ちます。 floatとして正確に表現できない最初の整数に到達する前に、符号付き整数オーバーフローUBをヒットしたためです。

ただし、x86-64 System V ABIを使用してgccやclangなどの実装向けにコンパイルする場合、実装で定義された別の選択肢のセットの下にあるUBは悪影響を及ぼしません。


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

ISO C++標準がfloatの動作についてどの程度明確であるか、および固定幅の指数と仮数フィールドに基づいていない形式が標準に準拠しているかどうかはわかりません。


コメントで:

@geza結果の番号を聞きたいです。

@nada:16777216

このループで16777216を出力/返すループがあると主張していますか?

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

もちろん、バグ修正されたバージョンでは、最初の整数である16777217notの前の値ではなく、正確に表現可能です。

(すべてのより高い浮動小数点値は正確な整数ですが、有効桁数よりも大きい指数値の場合、2、4、8の倍数などです。多くのより高い整数値を表すことができますが、最後に1単位あります。 (仮数部の)は1より大きいため、連続した整数ではありません。最大の有限floatは2 ^ 128未満であり、int64_tに対しても大きすぎます。)

コンパイラが元のループを終了して出力した場合は、コンパイラのバグです。

47
Peter Cordes

組み込み演算子!=は、そのオペランドが同じタイプである必要があり、必要に応じてプロモーションと変換を使用してそれを実現します。言い換えれば、あなたの状態は以下と同等です:

(float)i != (float)i

これは決して失敗してはならず、コードは最終的にiでオーバーフローし、プログラムに未定義の動作を与えます。したがって、あらゆる動作が可能です。

確認したいものを正しく確認するには、結果をintにキャストする必要があります。

if ((int)(float)i != i)