Можно ли в плавающей точке вернуть 0.0, вычитая два разных значения?

Из-за "приблизительной" природы с плавающей точкой возможно, что два разных набора значений возвращают одно и то же значение.

Пример:

#include <iostream>

int main() {
    std::cout.precision(100);

    double a = 0.5;
    double b = 0.5;
    double c = 0.49999999999999994;

    std::cout << a + b << std::endl; // output "exact" 1.0
    std::cout << a + c << std::endl; // output "exact" 1.0
}

Но возможно ли это и с помощью вычитания? Я имею в виду: есть два набора различных значений (сохраняя одно значение из них), которые возвращают 0.0?

то есть a - b = 0.0 и a - c = 0.0, учитывая некоторые наборы a,b и a,c с b != c??

Ответы

Ответ 1

Стандарт IEEE-754 был специально разработан таким образом, чтобы вычитание двух значений приводило к нулю тогда и только тогда, когда два значения равны, за исключением того, что вычитание бесконечности из самого себя дает NaN и/или исключение.

К сожалению, C++ не требует соответствия IEEE-754, и многие реализации C++ используют некоторые функции IEEE-754, но не полностью соответствуют.

Нередко поведение "сбрасывает" ненормальные результаты на ноль. Это является частью аппаратного обеспечения, позволяющего избежать бремени правильной обработки субнормальных результатов. Если это поведение действует, вычитание двух очень маленьких, но разных чисел может дать ноль. (Числа должны быть около нижней части нормального диапазона, имея несколько значащих бит в субнормальном диапазоне.)

Иногда системы с таким поведением могут предложить способ его отключения.

Другое поведение, о котором следует помнить, заключается в том, что C++ не требует, чтобы операции с плавающей точкой выполнялись точно так, как написано. Это позволяет использовать "избыточную точность" в промежуточных операциях и "сжатие" некоторых выражений. Например, a*b - c*d может быть вычислено с использованием одной операции, которая умножает a и b а затем другой, которая умножает c и d и вычитает результат из ранее вычисленного a*b. Эта последняя операция действует так, как если бы c*d вычислялись с бесконечной точностью, а не округлялись до номинального формата с плавающей запятой. В этом случае a*b - c*d может дать ненулевой результат, даже если a*b == c*d оценивается как true.

Некоторые реализации C++ предлагают способы отключить или ограничить такое поведение.

Ответ 2

Функция постепенного уменьшения значения стандарта IEEE с плавающей запятой предотвращает это. Постепенное понижение достигается за счет субнормальных (денормальных) чисел, которые расположены равномерно (в отличие от логарифмически, как нормальные числа с плавающей запятой) и расположены между наименьшими отрицательными и положительными нормальными числами с нулями в середине. Поскольку они равномерно распределены, сложение двух субнормальных чисел различной подписи (т.е. вычитание к нулю) является точным и, следовательно, не будет воспроизводить то, что вы просите. Наименьшее субнормальное (намного) меньше наименьшего расстояния между нормальными числами, и поэтому любое вычитание между неравными нормальными числами будет ближе к субнормальному, чем ноль.

Если вы отключите соответствие IEEE с помощью специального режима ЦП (DAZ) или сброса в ноль (FTZ) ЦП, то вы действительно можете вычесть два небольших близких числа, которые в противном случае привели бы к ненормальному числу, которое будет рассматриваться как ноль из-за режима работы процессора. Рабочий пример (Linux):

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);    // system specific
double d = std::numeric_limits<double>::min(); // smallest normal
double n = std::nextafter(d, 10.0);     // second smallest normal
double z = d - n;       // a negative subnormal (flushed to zero)
std::cout << (z == 0) << '\n' << (d == n);

Это должно напечатать

1
0

Первый 1 указывает, что результат вычитания равен нулю, а второй 0 указывает, что операнды не равны.

Ответ 3

К сожалению, ответ зависит от вашей реализации и способа ее настройки. C и C++ не требуют какого-либо конкретного представления или поведения с плавающей запятой. В большинстве реализаций используются представления IEEE 754, но они не всегда точно реализуют арифметическое поведение IEEE 754.

Чтобы понять ответ на этот вопрос, мы должны сначала понять, как работают числа с плавающей запятой.

Наивное представление с плавающей точкой будет иметь показатель степени, знак и мантиссу. Это значение будет

(-1) s 2 (e - e 0) (м /2 М)

Куда:

  • s - знаковый бит со значением 0 или 1.
  • е - поле экспоненты
  • е 0 - смещение экспоненты. По сути, он устанавливает общий диапазон чисел с плавающей запятой.
  • М - количество бит мантиссы.
  • m - мантисса со значением от 0 до 2 M -1

По концепции это похоже на научную нотацию, которой вас учили в школе.

Однако этот формат имеет много разных представлений одного и того же числа, тратится почти вся битовая область кодирования. Чтобы исправить это, мы можем добавить "неявную 1" к мантиссе.

(-1) s 2 (e - e 0) (1+ (м /2 М))

Этот формат имеет ровно одно представление каждого числа. Однако с этим есть проблема: он не может представлять ноль или числа, близкие к нулю.

Чтобы исправить это с плавающей точкой IEEE резервирует пару значений экспоненты для особых случаев. Значение нулевого показателя зарезервировано для представления небольших чисел, известных как субнормалы. Максимально возможное значение экспоненты зарезервировано для NaN и бесконечностей (которые я буду игнорировать в этом посте, поскольку они здесь не актуальны). Так что определение теперь становится.

(-1) s 2 (1 - e 0) (м /2 М), когда e = 0
(-1) s 2 (e - e 0) (1+ (м /2 М)), когда e> 0 и e <2 E -1

При таком представлении меньшие числа всегда имеют размер шага, который меньше или равен значению шага для больших. Таким образом, при условии, что результат вычитания меньше по величине, чем оба операнда, он может быть представлен точно. В частности, результаты, близкие, но не точно к нулю, могут быть представлены точно.

Это не применяется, если результат больше по величине, чем один или оба операнда, например вычитание небольшого значения из большого значения или вычитание двух значений противоположных знаков. В этих случаях результат может быть неточным, но он явно не может быть нулевым.

К сожалению, дизайнеры FPU срезали углы. Вместо того, чтобы включать логику для быстрой и правильной обработки субнормальных чисел, они либо вообще не поддерживали (ненулевые) субнормалы, либо обеспечивали медленную поддержку субнормалей, а затем давали пользователю возможность включать и выключать их. Если поддержка правильных субнормальных вычислений отсутствует или отключена, а число слишком мало для представления в нормализованной форме, оно будет "сброшено в ноль".

Таким образом, в реальном мире при некоторых системах и конфигурациях вычитание двух очень маленьких чисел с плавающей запятой может дать нулевой ответ.

Ответ 4

Исключая забавные цифры, такие как NAN, я не думаю, что это возможно.

Допустим, a и b являются нормальными конечными числами с плавающей запятой IEEE 754, а | a - b | меньше или равно обоим | a | и | б | (иначе это явно не ноль).

Это означает, что показатель степени <= и a, и b, и поэтому абсолютная точность, по крайней мере, так же высока, что делает вычитание точно представимым. Это означает, что если a - b == 0, то оно точно равно нулю, поэтому a == b.

Ответ 5

Можно ли в плавающей точке вернуть 0.0, вычитая два разных значения?

да, это возможно, к счастью

В остальном, много литературы о вычислениях с плавающей запятой.