Почему изменение порядка сумм возвращает другой результат?

Почему изменение порядка сумм возвращает другой результат?

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)