В арифметике с плавающей запятой, если два числа имеют одинаковое двоичное представление, то результат любой операции, выполняемой с этими числами, должен быть одинаковым, и сравнения на равенство с использованием ==
должны работать должным образом.
Например, если a и b вычисляются как 1.0/3.0
, они действительно будут иметь одно и то же двоичное представление в стандартной системе с плавающей запятой. Следовательно, x
и y
, рассчитанные следующим образом, должны быть идентичны и утверждение справедливо.
double a = 1.0/3.0;
double b = 1.0/3.0;
double x = a*a/Math::Pi;
double y = b*b/Math::Pi;
assert(x==y);
Мой вопрос: повлияет ли знак числа на точность результатов? Всегда ли будет верно следующее?
double a = 1.0/3.0;
double b = -1.0/3.0;
double x = a*a/Math::Pi;
double y = -(-b*b/Math::Pi);
assert(x==y);
Как насчет этого? Будет ли утверждение справедливым?
double a = 1.0/3.0;
double b = 1.0/7.0;
double x = a-b;
double y = -(b-a);
assert(x==y);
В основном я работаю на машинах x86/x64. Я думал, что C/C++/ASM будет вести себя одинаково, поэтому я пометил тегами C и C++.
Пожалуйста, не отмечайте нерелевантные языки. (Исправлено) /// А не будет ли это зависеть от оборудования? Вы ничего об этом не упомянули.
@WeijunZhou Могу ли я узнать, какой режим округления используется в процессоре x86/x64?
Режим округления @zmz можно настроить в реальном времени.
Числа с плавающей запятой в IEEE 754 представлены в виде бита знака, показателя степени и мантиссы. Таким образом, если все вычисления отличаются только знаковым битом, остальные биты должны быть идентичными для идентичных операций.
@Gene Не обязательно, 1./3.
и (-1.)/3.
могут не отличаться только знаком, когда, например, округление осуществляется в сторону положительной бесконечности. Режим округления вносит асимметрию.
Я искал информацию об этом в стандарте C++, а не в IEEE-754, но не смог найти, где стандарт C++ определяет модель с плавающей запятой. Стандарт C 2018 определяет модель в 5.2.4.2.2 3, согласно которой число с плавающей запятой определяется моделью s•b^e•sum(f[k]•b^-k для k = от 1 до p) , откуда видно, что изображенные числа симметричны относительно знака s. (Режимы округления приводят к асимметрии, а свободные спецификации для преобразования текстовых чисел в числа с плавающей запятой допускают асимметрию.) Может ли кто-нибудь указать на какое-нибудь место в стандарте C++, которое определяет подобную модель?
@EricPostpischil Раздел C++ [numeric.limits.members]
содержит сноску «lowest()
необходима, потому что не все представления с плавающей запятой имеют наименьшее (наиболее отрицательное) значение, которое является отрицательным из наибольшего (наиболее положительного) конечного значения». Поэтому я думаю, что нет намерения предписывать конкретную модель.
«и сравнения на равенство с использованием == должны работать должным образом». --> Если два значения имеют значение NAN, сравнение является ложным, даже с одинаковым битовым шаблоном.
TLDR: Теоретически знак может влиять на результаты, на практике — нет.
Стандарт чисел с плавающей запятой — IEEE-754 . Он указывает, что знак кодируется в отдельном бите. Это означает, что, в отличие от целых чисел, в которых используется дополнение до двух, диапазон точно симметричен относительно нуля. Ни мантисса, ни показатель степени не зависят от знака. Поэтому знак не влияет на точность.
Однако IEEE-754 позволяет различные режимы округления работать с младшим битом мантиссы (внутренне процессор выполняет вычисления с большим количеством битов, а затем округляет до ближайшего представления с плавающей запятой). Если вы измените режим округления на положительную или отрицательную бесконечность, это повлияет на результаты. Но это делается редко (если вообще когда-либо), и вам придется согласиться на такое изменение поведения; это не значение по умолчанию.
Даже если вы пишете библиотеку, вероятно, проще просто обязать ее вызывать из стандартной математической среды. В противном случае вам, возможно, придется беспокоиться о перехвате математических данных и передаче сигналов NaN. Не каждая аппаратная реализация также поддерживает изменение режима округления, например, графические процессоры обычно этого не делают или только с помощью явных инструкций (CUDA , OpenCL ). Не обращать внимания на это также соответствует поведению GCC по умолчанию:
Без каких-либо явных опций GCC предполагает округление до ближайшего или четного и не заботится о передаче сигналов NaN.
Строго говоря, большинство языков не требуют IEEE-754 для их типа с плавающей запятой. Однако вам будет сложно найти широко используемую систему, которая не соответствует стандарту. См. также Почему бы не использовать плавающую запятую на основе дополнения до двух?
Ожидание того, что диапазон симметричен, также заложено во многих кодах, включая стандартные библиотеки. Например, в C++ до C++11 было std::numeric_limits<float>::max()
для наибольшего конечного положительного значения и std::numeric_limits<float>::min()
для наименьшего ненулевого, нормализованного положительного значения. Если вам нужно было максимально отрицательное конечное значение, вам нужно было отрицать max()
.
Это изменилось только в C++11 с std::numeric_limits<float>::lowest()
, но не изменилось для самого C, который использует аналогичные макросы float.h. Он также по-прежнему оставляет пробелы, например, нет прямого способа получить первое нормализованное отрицательное число ниже нуля. Стандарт ожидает, что вы просто будете отрицать min()
.
«Но никто никогда этого не делает» — это чрезмерно и неверно. Есть люди, которые это делают.
Предпочтительный термин для дробной части представления с плавающей запятой — «значащее число». «Мантисса» — старый термин, обозначающий дробную часть логарифма. Значимые числа являются арифметическими; добавление к мантиссе увеличивает представленное значение в масштабе экспоненты. Мантисса логарифмическая; добавление к мантиссе умножает представленное значение.
@EricPostpischil спасибо за заметку о мантиссе! В моем родном немецком языке для этого значения обычно не используется, поэтому его трудно запомнить. По поводу округления, есть ли у вас пример, где это делается и по какой причине?
Режимы направленного округления можно использовать для интервальной арифметики, помогая в математических доказательствах границ и вычислениях с повышенной точностью. Такое использование встречается редко из-за затрат, связанных с реализацией выбора метода округления с использованием глобального регистра, который не требуется IEEE-754. У HP была архитектура, в которой метод округления можно было указать для каждой операции (в том числе для каждой линии SIMD), что делало ее гибкой для использования.
В качестве демонстрации того, какое значение может иметь режим округления, рассмотрим следующий код:
#include <stdio.h>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
int main()
{
double a = 1.0 / 3.0;
double b = -1.0 / 3.0;
if (a != -b) printf("unexpectedly unequal #1\n");
fesetround(FE_DOWNWARD);
a = 1.0 / 3.0;
b = -1.0 / 3.0;
if (a != -b) {
printf("unexpectedly unequal #2:\n");
printf("a = % .20f\n", a);
printf("b = % .20f\n", b);
}
}
При компиляции под clang v. 14.0.3 на моем Mac (как C, так и C++) этот код печатает «неожиданно неравный #2»,
со значениями a
и b
, отображаемыми как:
a = 0.33333333333333331482
b = -0.33333333333333337035
[Оглядываясь назад, я впечатлен тем, что это сработало именно так. Либо clang отказывается выполнять свертывание констант с плавающей запятой во время компиляции, либо оценивает эффект вызова fesetround
во время компиляции.]
[Также обратите внимание, что изменение режима округления повлияло на способ printf
отображения чисел. При обычном округлении это были бы 0.33333333333333331483
и -0.33333333333333337034
.]
Обновление: этот пример кода не работает (не печатает «неожиданно неравный #2») под gcc, я подозреваю, потому что gcc продолжает сворачивать константы во время компиляции. По крайней мере, в gcc v. 13.1.0 достаточно создать глобальные переменные double one = 1.0;
и double three = 3.0;
, а затем использовать one
и three
в различных вычислениях a
и b
.
Эксперименты показывают, что как только вы используете #pragma STDC FENV_ACCESS ON
, Clang перестает вычислять выражения с плавающей запятой во время компиляции. Это приятно; много лет назад это игнорировало FENV_ACCESS
.
@EricPostpischil Ага! Я так и подозревал, но мне было лень исследовать. Спасибо за подтверждение.
Это зависит от используемого режима округления.