web-dev-qa-db-ja.com

結合性の計算:(a + b)+ c!= a +(b + c)

最近、私は Eric Lippertによる古いブログ投稿 を調べていましたが、関連性について書いているときに、C#では_(a + b) + c_がa + (b + c)と同等ではないことを指摘していますa、b、cの値。

どのタイプと算術値の範囲が当てはまるのか、なぜそうであるのか理解できません。

53
Vaibhav Singla

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.Epsilondbl2 == 0

そして、文字通り質問を読んで:-)

checkedモードの場合:

checked
{
    int i1 = (int.MinValue + int.MaxValue) + int.MaxValue;
}

i1int.MaxValue

checked
{
    int temp = int.MaxValue;
    int i2 = int.MinValue + (temp + temp);
}

temp変数の使用に注意してください。そうしないと、コンパイラーが直接エラーを出します...技術的にはこれでも異なる結果になります:-)正しくコンパイルされるか、コンパイルされません)

これはOverflowExceptionをスローします...結果は異なります:-)(int.MaxValueException

78
xanatos

一例

a = 1e-30
b = 1e+30
c = -1e+30
14
Luka Rahne

極端な小さな数と大きな数で異なる結果がどのように得られるかを示す他の回答を拡張して、現実的な通常の数値を持つ浮動小数点が異なる回答を与える例を次に示します。

この場合、極端な精度の限界で数値を使用する代わりに、単純に多くの加算を行います。違いは、_(((...(((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
_
8
Duncan

これは、通常の値の型(int、longなど)が固定量のバイトを使用して格納されるという事実に由来します。したがって、2つの値の合計がバイトストレージ容量を超えると、オーバーフローが発生する可能性があります。

C#では、この種の問題を回避するために BigInteger を使用できます。 BigIntegerのサイズは任意であるため、オーバーフローは発生しません。

BigIntegerは、.NET 4.0以降(VS 2010以降)でのみ使用できます。

4
legrojan

同様の例をいくつか示します。

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に等しくなりますが、test24000000042Lになります。

0

短い答えは、数学的には(a + b) + c == a + (b + c)ですが、必ずしも計算上ではありません。

コンピューターは実際には2進数で動作することを思い出してください。単純な10進数でも、内部形式に変換すると丸めエラーが発生する可能性があります。

言語によっては、加算によっても丸めエラーが発生する可能性があり、上記の例では、a+bの丸めエラーがb+cの丸めエラーと異なる場合があります。

驚くべき違反者の1つはJavaScriptです:0.1 + 0.2 != 0.3。丸め誤差は、小数点以下のかなり長い距離ですが、現実的で問題があります。

最初に小さなパーツを追加することで丸め誤差を減らすことが一般的な原則です。このようにして、より大きな数に圧倒される前に蓄積することができます。

0
Manngo