浮動小数点の「おおよその」性質により、2つの異なる値のセットが同じ値を返す可能性があります。
例 :
_#include <iostream>
int main() {
std::cout.precision(100);
double a = 0.5;
double b = 0.5;
double c = 0.49999999999999994;
std::cout << a + b << std::endl; // output "exact" 1.0
std::cout << a + c << std::endl; // output "exact" 1.0
}
_
しかし、減算でそれは可能ですか?つまり、_0.0
_を返す2つの異なる値のセット(1つの値を維持)はありますか?
つまり、_a - b = 0.0
_および_a - c = 0.0
_と_a,b
_を組み合わせた_a,c
_および_b != c
_のセットがいくつか与えられます。
IEEE-754標準は、2つの値を減算するとゼロが生成されるように意図的に設計されています。
残念ながら、C++はIEEE-754への準拠を必要とせず、多くのC++実装はIEEE-754の一部の機能を使用しますが、完全には準拠していません。
珍しいことではない動作は、非正規の結果をゼロに「フラッシュ」することです。これは、異常な結果を正しく処理する負担を回避するためのハードウェア設計の一部です。この動作が有効になっている場合、2つの非常に小さいが異なる数値を減算すると、ゼロになる可能性があります。 (数値は、正常範囲の下部にある必要があり、非正常範囲に有効ビットがいくつかあります。)
この動作をするシステムは時々それを無効にする方法を提供するかもしれません。
注意すべきもう1つの動作は、C++では浮動小数点演算を記述どおりに実行する必要がないことです。これにより、中間演算や一部の式の「縮小」で「過剰な精度」を使用できます。たとえば、a*b - c*d
は、a
とb
を乗算する演算と、c
とd
を乗算して減算する演算を使用して計算できます。以前に計算されたa*b
の結果。この後者の演算は、c*d
が名目上の浮動小数点形式に丸められるのではなく、無限の精度で計算されたかのように動作します。この場合、a*b - c*d
がtrueと評価されても、a*b == c*d
はゼロ以外の結果を生成する場合があります。
一部のC++実装では、このような動作を無効化または制限する方法を提供しています。
IEEE浮動小数点標準の段階的なアンダーフロー機能により、これを防止します。漸進的なアンダーフローは、間隔をあけたsubnormal(denormal)数によって達成されます。均等に(通常の浮動小数点のように対数的にではなく)、中央にゼロがある最小の負の数と正の数の間にあります。それらは等間隔に配置されているため、符号が異なる2つの非正規数の加算(つまり、ゼロへの減算)は正確であり、したがって、要求した内容を再現できません。最小の非正規は、通常の数値間の最小距離よりも(はるかに)小さいため、等しくない正規の数値間の減算は、ゼロよりも非正規に近くなります。
特別なdenormals-are-zero(DAZ)またはflush-to-zero( CPUのFTZ)モードの場合、実際には、CPUのモードが原因でゼロとして扱われる非正規数になる2つの小さな近い数値を減算できます。 A 動作例 (Linux):
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); // system specific
double d = std::numeric_limits<double>::min(); // smallest normal
double n = std::nextafter(d, 10.0); // second smallest normal
double z = d - n; // a negative subnormal (flushed to zero)
std::cout << (z == 0) << '\n' << (d == n);
これは印刷する必要があります
1
0
最初の1は減算の結果が正確にゼロであることを示し、2番目の0はオペランドが等しくないことを示します。
残念ながら、答えは実装とその構成方法によって異なります。 CおよびC++は、特定の浮動小数点表現または動作を要求しません。ほとんどの実装はIEEE 754表現を使用しますが、IEEE 754算術動作を常に正確に実装するとは限りません。
この質問に対する答えを理解するには、まず浮動小数点数のしくみを理解する必要があります。
単純な浮動小数点表現には、指数、符号、仮数があります。その価値は
(-1)s2(e – e)(m/2M)
どこ:
これは、あなたが学校で教えた科学的記法と概念が似ています。
ただし、この形式には同じ数のさまざまな表現があり、符号化スペースのほぼ全体が無駄になります。これを修正するには、仮数に「暗黙の1」を追加します。
(-1)s2(e – e)(1+(m/2M))
この形式では、各数値の表現が1つだけあります。ただし、問題があるため、ゼロまたはゼロに近い数値を表すことはできません。
このIEEE浮動小数点を修正するために、特別な場合のためにいくつかの指数値が予約されています。ゼロの指数値は、非正規と呼ばれる小さな数値を表すために予約されています。可能な限り高い指数値はNaNと無限大のために予約されています(これらはここでは関係がないため、この投稿では無視します)。したがって、定義は次のようになります。
(-1)s2(1 – e)(m/2M)e = 0の場合
(-1)s2(e – e)(1+(m/2M))e> 0およびe <2の場合E-1
この表現では、小さい数のステップサイズは常に大きいもののステップサイズ以下になります。したがって、減算の結果の大きさが両方のオペランドよりも小さい場合、正確に表すことができます。特に、ゼロに近いが正確にゼロではない結果を正確に表すことができます。
これは、たとえば、大きな値から小さな値を減算したり、反対の符号の2つの値を減算したりするなど、結果が一方または両方のオペランドより大きい場合は適用されません。これらの場合、結果は不正確になる可能性がありますが、明らかにゼロにすることはできません。
残念ながら、FPUデザイナーは手抜きをしました。非正規数を迅速かつ正確に処理するロジックを含めるのではなく、(非ゼロの)非正規をまったくサポートしなかったか、非正規に対する遅いサポートを提供し、ユーザーにオンとオフを切り替えるオプションを提供しました。適切な非正規計算のサポートが存在しないか無効になっていて、その数が小さすぎて正規化された形式で表現できない場合、「ゼロにフラッシュ」されます。
したがって、現実の世界では、一部のシステムと構成では、2つの異なる非常に小さな浮動小数点数を減算すると、答えがゼロになる可能性があります。
NANのようなおかしい数字を除いて、それは可能ではないと思います。
Aとbが通常の有限のIEEE 754 floatであり、| a-b |であるとしましょう。 | a |の両方以下と| b | (そうでなければ、それは明らかにゼロではありません)。
これは、指数がaとbの両方で<=であるため、絶対精度が少なくとも同じであり、減算を正確に表現できることを意味します。つまり、a-b == 0の場合、それは正確にゼロであるため、a == bとなります。