Различия между операциями Mult и Div для чисел с плавающей запятой
Есть ли разница в точности вычислений для этих двух случаев:
1) x = y / 1000d;
2) x = y * 0.001d;
Изменить: не добавлять тег C#. Вопрос только с точки зрения "плавающей точки". Я не хочу знать, что быстрее, мне нужно знать, какой случай даст мне "лучшую точность".
4 ответа
Нет, они не одинаковы - по крайней мере, не с C#, с использованием версии, которая у меня есть на моей машине (просто стандарт.NET 4.5.1) на моем процессоре - там достаточно тонкостей, которые я не хотел бы требовать сделаю то же самое на всех машинах или на всех языках. В конце концов, это вполне может быть языковой вопрос.
Используя мой DoubleConverter
класс, чтобы показать точное значение double
и после нескольких бит проб и ошибок вот программа на C#, которая, по крайней мере, на моей машине показывает разницу:
using System;
class Program
{
static void Main(string[] args)
{
double input = 9;
double x1 = input / 1000d;
double x2 = input * 0.001d;
Console.WriteLine(x1 == x2);
Console.WriteLine(DoubleConverter.ToExactString(x1));
Console.WriteLine(DoubleConverter.ToExactString(x2));
}
}
Выход:
False
0.00899999999999999931998839741709161899052560329437255859375
0.009000000000000001054711873393898713402450084686279296875
Я могу воспроизвести это в C с помощью компилятора Microsoft C - извините, если это ужасный стиль C, но я думаю, что он, по крайней мере, демонстрирует различия:
#include <stdio.h>
void main(int argc, char **argv) {
double input = 9;
double x1 = input / 1000;
double x2 = input * 0.001;
printf("%s\r\n", x1 == x2 ? "Same" : "Not same");
printf("%.18f\r\n", x1);
printf("%.18f\r\n", x2);
}
Выход:
Not same
0.008999999999999999
0.009000000000000001
Я не изучал точные детали, но для меня имеет смысл, что есть разница, потому что деление на 1000 и умножение на "ближайший double
до 0,001"это не та же логическая операция... потому что 0,001 не может быть точно представлен как double
, Ближайший double
до 0,001 это на самом деле:
0.001000000000000000020816681711721685132943093776702880859375
... так вот что вы умножаете на. Вы теряете информацию рано и надеетесь, что она соответствует той же информации, которую вы теряете в противном случае при делении на 1000. Похоже, в некоторых случаях это не так.
Вы программируете на базе 10, но с плавающей точкой - на базе 2, вы МОЖЕТЕ представлять 1000 в базе 2, но не можете представлять 0,001 в базе 2, поэтому вы выбрали неверные числа, чтобы задать свой вопрос на компьютере x/1000!= x*0,001, Вы можете повезти большую часть времени с округлением и большей точностью, но это не математическая идентичность.
Теперь, возможно, это был ваш вопрос, может быть, вы хотели знать, почему х / 1000! = Х * 0,001. И ответ на этот вопрос заключается в том, что это двоичный компьютер, и он использует базу 2, а не базу 10, при переходе на базу 2 возникают проблемы с преобразованием 0,001, вы не можете точно представить эту дробь в числе с плавающей запятой IEEE.
В базе 10 мы знаем, что если в знаменателе есть дробь с множителем 3 (а в числителе ее нет, чтобы отменить ее), мы в конечном итоге получим бесконечно повторяющийся шаблон, в основном мы не можем точно представить это число с конечной набор цифр.
1/3 = 0,33333...
Та же проблема, когда вы пытаетесь представить 1/10 в основании 2. 10 = 2*5, 2 - это нормально, 1/2, но 5 - это настоящая проблема, 1/5.
1/10 (1/1000 работает так же). Элементарное длинное деление:
0 000110011
----------
1010 | 1.000000
1010
------
1100
1010
----
10000
1010
----
1100
1010
----
10
мы должны продолжать сбрасывать нули, пока не получим 10000 10 переходит в 16 один раз, остаток 6 опускается до следующего нуля. 10 входит в 12 1 оставшееся время 2. И мы повторяем схему, чтобы вы в конечном итоге повторяли это 001100110011 навсегда. Плавающая точка - это фиксированное число битов, поэтому мы не можем представить бесконечный паттерн.
Теперь, если ваш вопрос связан с чем-то вроде деления на 4, то же самое, что умножение на 1/4. Это другой вопрос. Ответ должен быть таким же, он потребляет больше циклов и / или логики для деления, чем для умножения, но в итоге получается с тем же ответом.
Нет, никакой разницы нет. За исключением случаев, если вы установили пользовательский режим округления.
GCC производит ((двойной)0,001 - (двойной)1,0/1000) == 0,0e0
Когда компилятор преобразует 0,001 в двоичный код, он делит 1 на 1000. Для этого используется программное моделирование с плавающей запятой, совместимое с целевой архитектурой.
Для высокой точности используются длинные двойные (80-битные) и программные симуляции любой точности.
PS Я использовал gcc для 64-битной машины, как sse, так и x87 FPU.
PPS С некоторыми оптимизациями 1/1000.0 может быть более точным для x87, поскольку x87 использует 80-битное внутреннее представление и 1000 == 1000.0. Это правда, если вы используете результат для следующих расчетов быстро. Если вы возвращаете / записываете в память, он вычисляет 80-битное значение, а затем округляет его до 64-битного. Но SSE чаще используется для двойного.
Возможно нет. Компилятор (или JIT), скорее всего, преобразует первый случай во второй, так как умножение обычно быстрее деления. Вам придется проверить это, скомпилировав код (с включенной оптимизацией или без нее), а затем исследуя сгенерированный IL с помощью такого инструмента, как IL Disassembler или.NET Reflector, и / или исследуя собственный код с помощью отладчика во время выполнения.