浮動小数点値を比較するときは注意する必要があることは一般的な知識です。通常、==
を使用する代わりに、イプシロンまたはULPベースの同等性テストを使用します。
ただし、==
を使用しても問題ない場合はありますか?
この単純なスニペットを見てください。どのケースが成功することが保証されていますか?
void fn(float a, float b) {
float l1 = a/b;
float l2 = a/b;
if (l1==l1) { } // case a)
if (l1==l2) { } // case b)
if (l1==a/b) { } // case c)
if (l1==5.0f/3.0f) { } // case d)
}
int main() {
fn(5.0f, 3.0f);
}
注: this および this をチェックしましたが、(すべて)のケースをカバーしていません。
注2:私はいくつかのプラス情報を追加する必要があるようですので、答えは実際に役立つ可能性があります:私は知りたい:
これは、私が 現在のドラフト標準 で見つけた唯一の関連するステートメントです。
浮動小数点型の値表現は実装定義です。 [注:このドキュメントでは、浮動小数点演算の精度に関する要件はありません。 [support.limits]も参照してください。 —メモを終了]
それで、これは「ケースa)」でさえ実装が定義されていることを意味するのでしょうか?つまり、l1==l1
は間違いなく浮動小数点演算です。したがって、実装が「不正確」である場合、l1==l1
はfalseになる可能性がありますか?
この質問は 浮動小数点==今でも大丈夫ですか? の複製ではないと思います。その質問は、私が尋ねているケースのいずれにも対処していません。同じテーマ、異なる質問。ケースa)-d)に具体的な回答が欲しいのですが、重複した質問には答えが見つかりません。
ただし、==を使用してもまったく問題ない場合はありますか?
確かにあります。例の1つのカテゴリは、計算を伴わない使用法です。変更時にのみ実行するセッター:
void setRange(float min, float max)
{
if(min == m_fMin && max == m_fMax)
return;
m_fMin = min;
m_fMax = max;
// Do something with min and/or max
emit rangeChanged(min, max);
}
不自然なケースは「機能する」場合があります。実用的なケースはまだ失敗する可能性があります。追加の問題の1つは、多くの場合、最適化により計算の方法にわずかな変動が生じるため、結果はシンボリックに等しいはずですが、数値的には異なることです。上記の例は、理論的にはそのような場合に失敗する可能性があります。一部のコンパイラは、パフォーマンスを犠牲にしてより一貫した結果を生成するオプションを提供します。私は、浮動小数点数の平等を避ける「常に」アドバイスします。
物理的な測定値とデジタルで保存されたフロートの平等性は、しばしば無意味です。したがって、フロートがコード内で等しいかどうかを比較すると、おそらく何か間違ったことをしていることになります。通常、それよりも大きい、または小さい、または許容範囲内にする必要があります。多くの場合、コードを書き直すことができるため、これらのタイプの問題は回避されます。
A)とb)のみが、同じ方法で導出され、float
精度に丸められた2つの値を比較するため、健全な実装で成功することが保証されています(詳細については、以下の法律を参照)。その結果、比較される値は両方とも最後のビットと同一であることが保証されます。
ケースc)およびd)は、計算とその後の比較がfloat
よりも高い精度で実行される可能性があるため、失敗する場合があります。 double
の異なる丸めは、テストに失敗するのに十分なはずです。
ただし、無限大またはNANが関係している場合、ケースa)およびb)は失敗する可能性があることに注意してください。
標準のN3242 C++ 11ワーキングドラフトを使用すると、次のことがわかります。
代入式を説明するテキストでは、型変換が行われることを明示的に述べられています[expr.ass] 3:
左オペランドがクラス型ではない場合、式は暗黙的に左オペランドのcv非修飾型に変換されます(4節)。
条項4は、標準変換[conv]を指します。これには、浮動小数点変換[conv.double] 1に関する以下が含まれます。
浮動小数点型のprvalueは、別の浮動小数点型のprvalueに変換できます。ソース値を宛先タイプで正確に表すことができる場合、変換の結果はその正確な表現になります。 ソース値が2つの隣接するデスティネーション値の間にある場合、変換の結果はそれらの値の実装定義の選択です。それ以外の場合、動作は未定義です。
(エンファシス鉱山。)
したがって、表現可能な範囲外の値(float a = 1e300
(UB)など)を処理しない限り、変換の結果が実際に定義されるという保証があります。
「内部浮動小数点表現は、コードで表示されるよりも正確である可能性がある」と考えるとき、標準[expr] 11の次の文について考えます。
浮動オペランドの値と浮動式の結果は、型が必要とするものよりも高い精度と範囲で表現できます。それによってタイプは変更されません。
これは、変数ではなく、オペランドと結果に適用されることに注意してください。これは、添付の脚注60で強調されています。
キャスト演算子と代入演算子は、5.4、5.2.9、および5.17で説明されている特定の変換を実行する必要があります。
(これは、コメントでMaciej Piechotkaが意味した脚注だと思います。番号は、彼が使用している標準のバージョンで変更されたようです。)
したがって、float a = some_double_expression;
と言うと、式の結果が実際にfloat
で表現できるように丸められるという保証があります(値が範囲外の場合のみUBを呼び出します)。 a
はその後、その丸められた値を参照します。
実装は、丸めの結果がランダムであることを実際に指定でき、したがってケースa)とb)を破ることができます。ただし、正常な実装ではそれはできません。
IEEE 754のセマンティクスを想定すると、これを行うことができる場合がいくつかあります。従来の浮動小数点数の計算は、可能な限り正確です。たとえば、オペランドと結果が整数であるすべての基本演算が含まれます(ただし、これに限定されません)。
そのため、表現できないものをもたらすようなことを何もしないという事実を知っているなら、あなたは大丈夫です。例えば
float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed
正確に表現できない(または正確でない操作を伴う)結果を伴う計算があり、操作の順序を変更した場合にのみ、状況は本当に悪くなります。
C++標準自体はIEEE 754セマンティクスを保証するものではないことに注意してください。しかし、それはほとんどの場合に対処できるものです。
a == b == 0.0
の場合、ケース(a)は失敗します。この場合、演算によりNaNが生成され、定義により(CではなくIEEE)NaN≠NaNになります。
ケース(b)および(c)は、このスレッドの実行中に浮動小数点ラウンドモード(または他の計算モード)が変更されると、並列計算で失敗する可能性があります。残念ながら、実際にこれを見てください。
ケース(d)は、コンパイラー(一部のマシン)が5.0f/3.0f
の計算を定数畳み込み、それを(不特定の精度の)定数結果で置き換えることを選択できるため、異なる場合がありますが、a/b
はターゲットマシンで実行時に計算されます(根本的に異なる場合があります)。実際、中間計算は任意の精度で実行できます。中間計算が80ビット浮動小数点で実行されたときに、古いIntelアーキテクチャで違いが見られました。これは、言語が直接サポートさえしていなかった形式です。
私の謙虚な意見では、==
演算子に依存するべきではありません。最大の問題は、丸めと精度の拡張です。 x86の場合、変数に格納できるよりも高い精度で浮動小数点演算を実行できます(コプロセッサーを使用している場合、IIRC [〜#〜] sse [〜#〜] 演算は同じ精度を使用しますストレージ)。
これは通常は良いことですが、次のような問題が発生します:1./2 != 1./2
最も単純なケースでは機能しますが、他の浮動小数点演算を追加すると、コンパイラーはいくつかの変数をスタックに分割し、値を変更して比較結果を変更することを決定できます。
100%の確実性を確保するには、アセンブリを見て、両方の値に対して以前に行われた操作を確認する必要があります。順序でさえ、重要なケースでは結果を変更できます。
全体的に==
を使用するポイントは何ですか?安定したアルゴリズムを使用する必要があります。つまり、値が等しくなくても機能しますが、同じ結果が得られます。 ==
が役立つ可能性のある唯一の場所は、必要な結果を正確に把握し、シリアル化を変更して目標をアーカイブできるシリアル化/非シリアル化です。