Почему изменение порядка сумм возвращает другой результат?
Почему изменение порядка сумм возвращает другой результат?
23.53 + 5.88 + 17.64
= 47.05
23.53 + 17.64 + 5.88
= 47.050000000000004
Оба Java и JavaScript возвращают те же результаты.
Я понимаю, что из-за того, что числа с плавающей запятой представлены в двоичном выражении, некоторые рациональные числа (например, 1/3 - 0.333333...) не могут быть представлены точно.
Почему простое изменение порядка элементов влияет на результат?
Ответы
Ответ 1
Возможно, этот вопрос глуп, но почему просто изменение порядка элементов влияет на результат?
Он изменит точки, в которых значения округлены, в зависимости от их величины. В качестве примера того, что мы видим, предположим, что вместо двоичной с плавающей запятой мы использовали десятичный тип с плавающей запятой с 4 значащими цифрами, где каждое добавление выполняется с "бесконечной" точностью, а затем округляется до ближайшего представимого числа. Вот две суммы:
1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
= 1.000 + 0.6667 (no rounding needed!)
= 1.667 (where 1.6667 is rounded to 1.667)
2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
= 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
= 1.666 (where 1.6663 is rounded to 1.666)
Нам даже не нужны нецелые числа для этой проблемы:
10000 + 1 - 10000 = (10000 + 1) - 10000
= 10000 - 10000 (where 10001 is rounded to 10000)
= 0
10000 - 10000 + 1 = (10000 - 10000) + 1
= 0 + 1
= 1
Это, возможно, более ясно показывает, что важная часть состоит в том, что мы имеем ограниченное число значащих цифр - не ограниченное число десятичных знаков. Если бы мы всегда могли хранить одинаковое количество десятичных знаков, то с добавлением и вычитанием, по крайней мере, мы были бы в порядке (до тех пор, пока значения не переполнились). Проблема в том, что когда вы получаете больше номеров, теряется меньшая информация - 10001 округляется до 10000 в этом случае. (Это пример проблемы, которую отметил Эрик Липперт в своем ответе.)
Важно отметить, что значения в первой строке правой стороны одинаковы во всех случаях - поэтому, хотя важно понимать, что ваши десятичные числа (23.53, 5.88, 17.64) не будут представлены точно так же, как double
, это только проблема из-за проблем, показанных выше.
Ответ 2
Вот что происходит в двоичном формате. Как известно, некоторые значения с плавающей запятой не могут быть представлены точно в двоичном формате, даже если они могут быть представлены точно в десятичной форме. Эти 3 числа являются лишь примерами этого факта.
С помощью этой программы я выводим шестнадцатеричные представления каждого числа и результаты каждого добавления.
public class Main{
public static void main(String args[]) {
double x = 23.53; // Inexact representation
double y = 5.88; // Inexact representation
double z = 17.64; // Inexact representation
double s = 47.05; // What math tells us the sum should be; still inexact
printValueAndInHex(x);
printValueAndInHex(y);
printValueAndInHex(z);
printValueAndInHex(s);
System.out.println("--------");
double t1 = x + y;
printValueAndInHex(t1);
t1 = t1 + z;
printValueAndInHex(t1);
System.out.println("--------");
double t2 = x + z;
printValueAndInHex(t2);
t2 = t2 + y;
printValueAndInHex(t2);
}
private static void printValueAndInHex(double d)
{
System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
}
}
Метод printValueAndInHex
- это только хелпер-принтер.
Выход выглядит следующим образом:
403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004
Первые 4 числа: x
, y
, z
и s
шестнадцатеричные представления. В представлении с плавающей точкой IEEE биты 2-12 представляют собой двоичный показатель, то есть масштаб числа. (Первый бит - бит знака, а остальные биты для мантиссы.) Показанный экспонента - это фактически двоичное число минус 1023.
Экспоненты для первых 4 чисел извлекаются:
sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5
Первый набор дополнений
Второе число (y
) имеет меньшую величину. При добавлении этих двух чисел для получения x + y
последние 2 бита второго номера (01
) смещаются за пределы диапазона и не фигурируют в расчете.
Второе добавление добавляет x + y
и z
и добавляет два числа того же масштаба.
Второй набор дополнений
Здесь сначала встречается x + z
. Они имеют одинаковую шкалу, но они дают число, которое выше по шкале:
404 => 0|100 0000 0100| => 1028 - 1023 = 5
Второе добавление добавляет x + z
и y
, и теперь из бита y
удаляются 3 бита, чтобы добавить числа (101
). Здесь должен быть округлен вверх, потому что результатом является следующее число с плавающей запятой вверх: 4047866666666666
для первого набора дополнений против 4047866666666667
для второго набора дополнений. Эта ошибка достаточно значительна, чтобы отображать ее в распечатке.
В заключение, будьте осторожны при выполнении математических операций с номерами IEEE. Некоторые представления неточны, и они становятся еще более неточными, когда весы различны. Добавьте и вычитайте числа одинакового масштаба, если сможете.
Ответ 3
Ответ Джона, конечно, правильный. В вашем случае ошибка не больше, чем ошибка, которую вы накопили, делая любую операцию с плавающей запятой. У вас есть сценарий, когда в одном случае вы получаете нулевую ошибку, а в другой вы получаете крошечную ошибку; что на самом деле это не интересный сценарий. Хороший вопрос: Существуют ли сценарии, в которых изменение порядка вычислений происходит от крошечной ошибки до (относительно) огромной ошибки? Ответ однозначно да.
Рассмотрим, например:
x1 = (a - b) + (c - d) + (e - f) + (g - h);
против
x2 = (a + c + e + g) - (b + d + f + h);
против
x3 = a - b + c - d + e - f + g - h;
Очевидно, что в точной арифметике они были бы одинаковыми. Интересно попытаться найти значения для a, b, c, d, e, f, g, h так, чтобы значения x1 и x2 и x3 отличались большой величиной. Посмотрите, можете ли вы это сделать!
Ответ 4
Числа с плавающей запятой представлены в формате IEEE 754, который обеспечивает определенный размер бит для мантиссы (значение). К сожалению, это дает вам определенное количество "фракционных строительных блоков" для игры, и определенные дробные значения не могут быть точно представлены.
Что происходит в вашем случае, так это то, что во втором случае добавление, вероятно, связано с некоторой проблемой точности из-за того, что порядок оценивается. Я не рассчитал значения, но может быть, например, что 23.53 + 17.64 не могут быть точно представлены, в то время как 23.53 + 5.88 может.
К сожалению, это известная проблема, с которой вам просто нужно иметь дело.
Ответ 5
Это на самом деле охватывает гораздо больше, чем просто Java и Javascript, и, вероятно, повлияет на любой язык программирования с использованием поплавков или удвоений.
В памяти плавающие точки используют специальный формат в строках IEEE 754 (конвертер предоставляет гораздо лучшее объяснение, чем я могу).
В любом случае, здесь плавающий конвертер.
http://www.h-schmidt.net/FloatConverter/
Вещь о порядке операций - это "тонкость" операции.
Ваша первая строка дает 29.41 из первых двух значений, что дает нам 2 ^ 4 в качестве показателя.
Вторая строка дает 41.17, что дает нам 2 ^ 5 как показатель степени.
Мы теряем значительную цифру, увеличивая показатель, который, вероятно, изменит результат.
Попробуйте пометить последний бит в крайнем правом углу влево и вправо для 41.17, и вы увидите, что что-то как "незначительное", как 1/2 ^ 23 экспонента, будет достаточно, чтобы вызвать эту разницу в плавающей запятой.
Изменить: для тех из вас, кто помнит важные цифры, это подпадает под эту категорию. 10 ^ 4 + 4999 со значительной цифрой 1 будет 10 ^ 4. В этом случае значительная цифра намного меньше, но мы можем видеть результаты с прикрепленным к ней .00000000004.
Ответ 6
Я считаю, что это связано с порядком эвалеции. Хотя сумма естественно одинакова в математическом мире, в бинарном мире вместо A + B + C = D, это
A + B = E
E + C = D(1)
Итак, есть дополнительный шаг, на котором числа с плавающей запятой могут выйти.
При изменении порядка
A + C = F
F + B = D(2)
Ответ 7
Я рекомендую посмотреть это замечательное видео, которое объясняет, как арифметика с плавающей запятой работает в Javascript (и на других языках тоже).
Все, что вы никогда не хотели знать о номерах JavaScript