Влияет ли знак на точность и точность чисел с плавающей запятой?

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

Например, если 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++.

Это зависит от используемого режима округления.

Weijun Zhou 16.08.2024 07:47

Пожалуйста, не отмечайте нерелевантные языки. (Исправлено) /// А не будет ли это зависеть от оборудования? Вы ничего об этом не упомянули.

ikegami 16.08.2024 07:48

@WeijunZhou Могу ли я узнать, какой режим округления используется в процессоре x86/x64?

zmz 16.08.2024 08:05

Режим округления @zmz можно настроить в реальном времени.

ALX23z 16.08.2024 08:37

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

Gene 16.08.2024 08:38
en.cppreference.com/w/cpp/numeric/fenv/FE_round
Weijun Zhou 16.08.2024 08:39

@Gene Не обязательно, 1./3. и (-1.)/3. могут не отличаться только знаком, когда, например, округление осуществляется в сторону положительной бесконечности. Режим округления вносит асимметрию.

Weijun Zhou 16.08.2024 08:41

Я искал информацию об этом в стандарте 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++, которое определяет подобную модель?

Eric Postpischil 16.08.2024 14:13

@EricPostpischil Раздел C++ [numeric.limits.members] содержит сноску «lowest() необходима, потому что не все представления с плавающей запятой имеют наименьшее (наиболее отрицательное) значение, которое является отрицательным из наибольшего (наиболее положительного) конечного значения». Поэтому я думаю, что нет намерения предписывать конкретную модель.

Homer512 16.08.2024 15:38

«и сравнения на равенство с использованием == должны работать должным образом». --> Если два значения имеют значение NAN, сравнение является ложным, даже с одинаковым битовым шаблоном.

chux - Reinstate Monica 17.08.2024 17:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
10
120
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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().

«Но никто никогда этого не делает» — это чрезмерно и неверно. Есть люди, которые это делают.

Eric Postpischil 16.08.2024 14:14

Предпочтительный термин для дробной части представления с плавающей запятой — «значащее число». «Мантисса» — старый термин, обозначающий дробную часть логарифма. Значимые числа являются арифметическими; добавление к мантиссе увеличивает представленное значение в масштабе экспоненты. Мантисса логарифмическая; добавление к мантиссе умножает представленное значение.

Eric Postpischil 16.08.2024 14:15

@EricPostpischil спасибо за заметку о мантиссе! В моем родном немецком языке для этого значения обычно не используется, поэтому его трудно запомнить. По поводу округления, есть ли у вас пример, где это делается и по какой причине?

Homer512 16.08.2024 14:57

Режимы направленного округления можно использовать для интервальной арифметики, помогая в математических доказательствах границ и вычислениях с повышенной точностью. Такое использование встречается редко из-за затрат, связанных с реализацией выбора метода округления с использованием глобального регистра, который не требуется IEEE-754. У HP была архитектура, в которой метод округления можно было указать для каждой операции (в том числе для каждой линии SIMD), что делало ее гибкой для использования.

Eric Postpischil 16.08.2024 15:31
Ответ принят как подходящий

В качестве демонстрации того, какое значение может иметь режим округления, рассмотрим следующий код:

#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.

Eric Postpischil 16.08.2024 19:45

@EricPostpischil Ага! Я так и подозревал, но мне было лень исследовать. Спасибо за подтверждение.

Steve Summit 16.08.2024 20:11

Другие вопросы по теме

Можно ли с уверенностью предположить, что 32-битные числа с плавающей запятой можно напрямую сравнивать друг с другом, если значение соответствует мантиссе?
Разница между 0f и 0 на Vector2 и Vector3 в Unity
Почему я получаю большие случайные значения при использовании scanf("%f", &intVar)? Как входные данные с плавающей запятой преобразуются в эти значения?
Почему коэффициент разницы, где h = FLT_MIN, всегда равен 0? FLT_MIN теряется в неточностях с плавающей запятой?
Как справиться с переполнением в двойных скалярах при работе с очень большими числами в Python?
NASM x64: часть с плавающей запятой неправильно напечатана как 0,000000
Преобразование числа с плавающей запятой в 16-битное двоичное значение в C
Преобразование с потерями между длинными двойными и двойными
Неверный расчет математического выражения с использованием sin и cos в C?
Как округление работает при умножении с плавающей запятой?