Я работаю над интерпретатором LISP и реализовал рациональные числа. Я думал, что они имеют преимущество перед двойными, чтобы иметь возможность представлять числа как 1/3. Я сделал несколько расчетов, чтобы сравнить результаты. Я был удивлен результатами
с двойниками
(* 3.0 (/ 1.0 3.0)) -> 1
(* 3.0 (/ 4.0 3.0)) -> 4
(* 81.0 (/ 1.0 81.0)) -> 1
с соотношениями:
(* 3 (/ 1 3)) -> 1
(* 3 (/ 4 3)) -> 4
(* 81 (/ 1 81)) -> 1
Почему результаты операций с плавающей запятой точны? Должна быть потеря точности. double не может хранить бесконечное количество цифр. Или я что-то пропустил?
Я сделал быстрый тест с небольшим C-приложением. Тот же результат.
#include <stdio.h>
int main()
{
double a1 = 1, b1 = 3;
double a2 = 1, b2 = 81;
printf("result : %f\n", a1 / b1 * b1);
printf("result : %f\n", a2 / b2 * b2);
return 0;
}
Выход:
результат: 1.000000
результат: 1.000000
MFG
Мартин
В первом случае точный результат умножения находится на полпути между 1,0 и наибольшим двойным числом, которое меньше 1,0. В соответствии с правилами округления до ближайшего стандарта IEEE 754, половина значений округляется до четного, в данном случае до 1,0. По сути, округление результата умножения отменяет ошибку, вносимую округлением результата деления.
Эта Java-программа иллюстрирует происходящее. Все преобразования в BigDecimal и BigDecimal арифметические операции являются точными:
import java.math.BigDecimal;
public class Test {
public static void main(String[] args) {
double a1 = 1, b1 = 3;
System.out.println("Final Result: " + ((a1 / b1) * b1));
BigDecimal divResult = new BigDecimal(a1 / b1);
System.out.println("Division Result: " + divResult);
BigDecimal multiplyResult = divResult.multiply(BigDecimal.valueOf(3));
System.out.println("Multiply Result: " + multiplyResult);
System.out.println("Error rounding up to 1.0: "+ BigDecimal.valueOf(1).subtract(multiplyResult));
BigDecimal nextDown = new BigDecimal(Math.nextAfter(1.0, 0));
System.out.println("Next double down from 1.0: " + nextDown);
System.out.println("Error rounding down: "+ multiplyResult.subtract(nextDown));
}
}
Выход:
Final Result: 1.0
Division Result: 0.333333333333333314829616256247390992939472198486328125
Multiply Result: 0.999999999999999944488848768742172978818416595458984375
Error rounding up to 1.0: 5.5511151231257827021181583404541015625E-17
Next double down from 1.0: 0.99999999999999988897769753748434595763683319091796875
Error rounding down: 5.5511151231257827021181583404541015625E-17
Выход для второго, похожего, случая:
Final Result: 1.0
Division Result: 0.012345679012345678327022824305458925664424896240234375
Multiply Result: 0.9999999999999999444888487687421729788184165954589843750
Error rounding up to 1.0: 5.55111512312578270211815834045410156250E-17
Next double down from 1.0: 0.99999999999999988897769753748434595763683319091796875
Error rounding down: 5.55111512312578270211815834045410156250E-17
Эта программа иллюстрирует ситуацию, в которой может накапливаться ошибка округления:
import java.math.BigDecimal;
public class Test {
public static void main(String[] args) {
double tenth = 0.1;
double sum = 0;
for (int i = 0; i < 10; i++) {
sum += tenth;
}
System.out.println("Sum: " + new BigDecimal(sum));
System.out.println("Product: " + new BigDecimal(10.0 * tenth));
}
}
Выход:
Sum: 0.99999999999999988897769753748434595763683319091796875
Product: 1
Умножение на 10 раундов до 1,0. Выполнение того же умножения путем повторного сложения не дает точного ответа.