Расширение на другие ответы, которые показывают, как с крайними малых и больших чисел вы получить другой результат, вот пример, где плавающая точка с реалистичными нормальными номерами дает вам другой ответ.
В этом случае вместо чисел с предельными пределами точности я просто делаю много дополнений. Разница между (((...(((a+b)+c)+d)+e)...
или ...(((a+b)+(c+d))+((e+f)+(g+h)))+...
Я использую python здесь, но вы, вероятно, получите те же результаты, если вы напишете это на C#. Сначала создайте список из миллиона значений, каждый из которых равен 0,1. Добавьте их с левой стороны, и вы видите ошибки округления становятся существенными:
>>> 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]
А вот простой pair_sum написанный на 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));
}
}
}
с выходом:
100000.000001333
100000
Возможно, значения с плавающей запятой ... – xanatos
FWIW, эта проблема не является C# конкретной. Математика с плавающей точкой, реализованная в аппаратных средствах, не совсем ассоциативна. Действительно, это не только аппаратное обеспечение, но и любая реализация с плавающей запятой, соответствующая IEEE 754. Если вам нужна ассоциативность для реальных чисел, вам нужно придумать свой собственный формат (и, вероятно, нанять математика в этом процессе) – slebetman
@slebetman Любая система, использующая фиксированное количество значимых цифр не будет ассоциативным. Если мы используем одну значимую цифру, например, округление после каждой операции, мы имеем: (1 + 0,5) + 0,5 == 1.5 (округлено до 2) + 0,5 == 2,5 (округлено до 3) == 3, а 1 + (0,5 + 0,5) == 1 + 1 == 2. IEEE 754 просто использует двоичные цифры вместо десятичных цифр с фиксированным количество значащих цифр (мантисса) – xanatos