float
が大きな整数を正確に表す機能を失う場所を調べてみました。だから私はこの小さなスニペットを書きました:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
このコードは、clangを除くすべてのコンパイラで機能するようです。 Clangは単純な無限ループを生成します。 ゴッドボルト 。
これは許可されますか?はいの場合、それはQoIの問題ですか?
@ 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 つまり、このバグのあるコードで確認したかったことです。
もちろん、バグ修正されたバージョンでは、最初の整数である16777217
がnotの前の値ではなく、正確に表現可能です。
(すべてのより高い浮動小数点値は正確な整数ですが、有効桁数よりも大きい指数値の場合、2、4、8の倍数などです。多くのより高い整数値を表すことができますが、最後に1単位あります。 (仮数部の)は1より大きいため、連続した整数ではありません。最大の有限float
は2 ^ 128未満であり、int64_t
に対しても大きすぎます。)
コンパイラが元のループを終了して出力した場合は、コンパイラのバグです。
組み込み演算子!=
は、そのオペランドが同じタイプである必要があり、必要に応じてプロモーションと変換を使用してそれを実現します。言い換えれば、あなたの状態は以下と同等です:
(float)i != (float)i
これは決して失敗してはならず、コードは最終的にi
でオーバーフローし、プログラムに未定義の動作を与えます。したがって、あらゆる動作が可能です。
確認したいものを正しく確認するには、結果をint
にキャストする必要があります。
if ((int)(float)i != i)