web-dev-qa-db-fra.com

Mathématiques d'associativité: (a + b) + c! = A + (b + c)

Récemment, je traversais un ancien billet de blog d'Eric Lippert dans lequel, tout en écrivant sur l'associativité, il mentionne qu'en C #, (a + b) + c n'est pas équivalent à a + (b + c) pour certaines valeurs de a, b, c.

Je ne suis pas en mesure de comprendre pour quels types et plage de valeurs arithmétiques cela pourrait être vrai et pourquoi.

53
Vaibhav Singla

Sur la plage du type double:

double dbl1 = (double.MinValue + double.MaxValue) + double.MaxValue;
double dbl2 = double.MinValue + (double.MaxValue + double.MaxValue);

Le premier est double.MaxValue, le second est double.Infinity

Sur la précision du type double:

double dbl1 = (double.MinValue + double.MaxValue) + double.Epsilon;
double dbl2 = double.MinValue + (double.MaxValue + double.Epsilon);

Maintenant, dbl1 == double.Epsilon, alors que dbl2 == 0.

Et en lisant littéralement la question :-)

En mode checked:

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

i1 est int.MaxValue

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

(notez l'utilisation de la variable temp, sinon le compilateur donnera une erreur directement ... Techniquement, même ce serait un résultat différent :-) Compile correctement vs ne compile pas)

cela jette une OverflowException... Les résultats sont différents :-) (int.MaxValue vs Exception)

78
xanatos

un exemple

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

Répondant aux autres réponses qui montrent comment, avec les extrêmes de petits et de grands nombres, vous obtenez un résultat différent, voici un exemple où une virgule flottante avec des nombres normaux réalistes vous donne une réponse différente.

Dans ce cas, au lieu d’utiliser des nombres aux limites extrêmes de la précision, je fais simplement beaucoup d’additions. La différence est entre faire (((...(((a+b)+c)+d)+e)... ou ...(((a+b)+(c+d))+((e+f)+(g+h)))+...

J'utilise python ici, mais vous obtiendrez probablement les mêmes résultats si vous écrivez cela en C #. Commencez par créer une liste d'un million de valeurs, toutes égales à 0,1. Additionnez-les en partant de la gauche et vous constaterez que les erreurs d'arrondi deviennent significatives:

>>> numbers = [0.1]*1000000
>>> sum(numbers)
100000.00000133288

Maintenant, ajoutez-les à nouveau, mais cette fois-ci, ajoutez-les par paires (il existe des moyens beaucoup plus efficaces de faire cela qui utilisent moins de stockage intermédiaire, mais j'ai gardé la mise en œuvre simple ici):

>>> 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

Cette fois, les erreurs d'arrondis sont réduites au minimum.

Éditer pour compléter, voici une implémentation plus efficace mais moins facile à suivre d’une somme par paire. Il donne la même réponse que la pair_sum() ci-dessus:

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]

Et voici la simple paire_sum écrite en C #:

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));
        }
    }
}

avec sortie:

100000.000001333
100000
8
Duncan

Cela provient du fait que les types de valeurs ordinaires (int, long, etc.) sont stockés en utilisant un nombre d'octets fixe. Un dépassement de capacité est ainsi possible lorsque la somme de deux valeurs dépasse la capacité de stockage en octets.

En C #, on peut utiliser BigInteger pour éviter ce genre de problème. Les BigInteger ont une taille arbitraire et ne créent donc pas de débordement. 

BigInteger est uniquement disponible à partir de .NET 4.0 et versions ultérieures (VS 2010+).

4
legrojan

Quelques exemples similaires:

static void A(string s, int i, int j)
{
  var test1 = (s + i) + j;
  var test2 = s + (i + j);
  var testX = s + i + j;
}

Ici, A("Hello", 3, 5) conduit à test1 et testX étant égaux à "Hello35", alors que test2 sera "Hello8".

Et:

static void B(int i, int j, long k)
{
  var test1 = (i + j) + k;
  var test2 = i + (j + k);
  var testX = i + j + k;
}

Ici, B(2000000000, 2000000000, 42L) conduit à test1 et testX étant égaux à -294967254L en mode unchecked habituel, alors que test2 devient 4000000042L.

0
Jeppe Stig Nielsen

La réponse courte est (a + b) + c == a + (b + c) mathématiquement, mais pas nécessairement de calcul.

En se rappelant que les ordinateurs fonctionnent vraiment en binaire, même des décimales simples peuvent entraîner des erreurs d'arrondi lors de la conversion au format interne.

Selon la langue, même un ajout peut entraîner des erreurs d'arrondi. Dans l'exemple ci-dessus, l'erreur d'arrondi dans a+b peut différer de celle dans b+c.

JavaScript est surprenant: 0.1 + 0.2 != 0.3. L'erreur d'arrondi est très loin dans la décimale, mais réelle et problématique.

En règle générale, vous réduisez les erreurs d’arrondi en ajoutant d’abord les petites pièces. De cette façon, ils peuvent s'accumuler avant d'être submergés par le plus grand nombre.

0
Manngo