最近、私は Eric Lippertによる古いブログ投稿 を調べていましたが、関連性について書いているときに、C#では_(a + b) + c
_がa + (b + c)
と同等ではないことを指摘していますa、b、cの値。
どのタイプと算術値の範囲が当てはまるのか、なぜそうであるのか理解できません。
double
タイプの範囲:
double dbl1 = (double.MinValue + double.MaxValue) + double.MaxValue;
double dbl2 = double.MinValue + (double.MaxValue + double.MaxValue);
最初はdouble.MaxValue
、2番目はdouble.Infinity
double
タイプの精度について:
double dbl1 = (double.MinValue + double.MaxValue) + double.Epsilon;
double dbl2 = double.MinValue + (double.MaxValue + double.Epsilon);
今dbl1 == double.Epsilon
、dbl2 == 0
。
そして、文字通り質問を読んで:-)
checked
モードの場合:
checked
{
int i1 = (int.MinValue + int.MaxValue) + int.MaxValue;
}
i1
はint.MaxValue
checked
{
int temp = int.MaxValue;
int i2 = int.MinValue + (temp + temp);
}
(temp
変数の使用に注意してください。そうしないと、コンパイラーが直接エラーを出します...技術的にはこれでも異なる結果になります:-)正しくコンパイルされるか、コンパイルされません)
これはOverflowException
をスローします...結果は異なります:-)(int.MaxValue
対Exception
)
一例
a = 1e-30
b = 1e+30
c = -1e+30
極端な小さな数と大きな数で異なる結果がどのように得られるかを示す他の回答を拡張して、現実的な通常の数値を持つ浮動小数点が異なる回答を与える例を次に示します。
この場合、極端な精度の限界で数値を使用する代わりに、単純に多くの加算を行います。違いは、_(((...(((a+b)+c)+d)+e)...
_または...(((a+b)+(c+d))+((e+f)+(g+h)))+...
を実行することです。
ここではpython=を使用していますが、C#で記述しても同じ結果が得られるでしょう。最初に、すべてが0.1である100万個の値のリストを作成します。左側と丸め誤差が大きくなることがわかります:
_>>> numbers = [0.1]*1000000
>>> sum(numbers)
100000.00000133288
_
ここでそれらを再度追加しますが、今回はペアで追加します(これを行うには、中間ストレージをあまり使用しないはるかに効率的な方法がありますが、ここでは実装を単純に保ちました)。
_>>> def pair_sum(numbers):
if len(numbers)==1:
return numbers[0]
if len(numbers)%2:
numbers.append(0)
return pair_sum([a+b for a,b in Zip(numbers[::2], numbers[1::2])])
>>> pair_sum(numbers)
100000.0
_
今回は、丸め誤差は最小限に抑えられます。
編集完全を期すために、ペアワイズサムの実装の追跡は、より効率的ですが、簡単ではありません。上記のpair_sum()
と同じ答えが得られます。
_def pair_sum(seq):
tmp = []
for i,v in enumerate(seq):
if i&1:
tmp[-1] = tmp[-1] + v
i = i + 1
n = i & -i
while n > 2:
t = tmp.pop(-1)
tmp[-1] = tmp[-1] + t
n >>= 1
else:
tmp.append(v)
while len(tmp) > 1:
t = tmp.pop(-1)
tmp[-1] = tmp[-1] + t
return tmp[0]
_
そして、これがC#で書かれた簡単なpair_sumです:
_using System;
using System.Linq;
namespace ConsoleApplication1
{
class Program
{
static double pair_sum(double[] numbers)
{
if (numbers.Length==1)
{
return numbers[0];
}
var new_numbers = new double[(numbers.Length + 1) / 2];
for (var i = 0; i < numbers.Length - 1; i += 2) {
new_numbers[i / 2] = numbers[i] + numbers[i + 1];
}
if (numbers.Length%2 != 0)
{
new_numbers[new_numbers.Length - 1] = numbers[numbers.Length-1];
}
return pair_sum(new_numbers);
}
static void Main(string[] args)
{
var numbers = new double[1000000];
for (var i = 0; i < numbers.Length; i++) numbers[i] = 0.1;
Console.WriteLine(numbers.Sum());
Console.WriteLine(pair_sum(numbers));
}
}
}
_
出力あり:
_100000.000001333
100000
_
これは、通常の値の型(int、longなど)が固定量のバイトを使用して格納されるという事実に由来します。したがって、2つの値の合計がバイトストレージ容量を超えると、オーバーフローが発生する可能性があります。
C#では、この種の問題を回避するために BigInteger を使用できます。 BigIntegerのサイズは任意であるため、オーバーフローは発生しません。
BigIntegerは、.NET 4.0以降(VS 2010以降)でのみ使用できます。
同様の例をいくつか示します。
static void A(string s, int i, int j)
{
var test1 = (s + i) + j;
var test2 = s + (i + j);
var testX = s + i + j;
}
ここでA("Hello", 3, 5)
はtest1
およびtestX
が"Hello35"
と等しいことにつながりますが、test2
は"Hello8"
になります。
そして:
static void B(int i, int j, long k)
{
var test1 = (i + j) + k;
var test2 = i + (j + k);
var testX = i + j + k;
}
ここでB(2000000000, 2000000000, 42L)
はtest1
につながり、testX
は通常のunchecked
モードで-294967254L
に等しくなりますが、test2
は4000000042L
になります。
短い答えは、数学的には(a + b) + c == a + (b + c)
ですが、必ずしも計算上ではありません。
コンピューターは実際には2進数で動作することを思い出してください。単純な10進数でも、内部形式に変換すると丸めエラーが発生する可能性があります。
言語によっては、加算によっても丸めエラーが発生する可能性があり、上記の例では、a+b
の丸めエラーがb+c
の丸めエラーと異なる場合があります。
驚くべき違反者の1つはJavaScriptです:0.1 + 0.2 != 0.3
。丸め誤差は、小数点以下のかなり長い距離ですが、現実的で問題があります。
最初に小さなパーツを追加することで丸め誤差を減らすことが一般的な原則です。このようにして、より大きな数に圧倒される前に蓄積することができます。