Другой результат с плавающей запятой при включенной оптимизации - ошибка компилятора?


109

Приведенный ниже код работает в Visual Studio 2008 с оптимизацией и без нее. Но работает только на g ++ без оптимизации (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Результат должен быть:

4.5
4.6

Но g ++ с оптимизацией ( O1- O3) выведет:

4.5
4.5

Если я добавлю volatileключевое слово перед t, оно сработает, может быть какая-то ошибка оптимизации?

Тестируйте на g ++ 4.1.2 и 4.4.4.

Вот результат на ideone: http://ideone.com/Rz937

И вариант, который я тестирую на g ++, прост:

g++ -O2 round.cpp

Более интересный результат, даже если я включаю /fp:fastопцию в Visual Studio 2008, результат все равно правильный.

Дальнейший вопрос:

Мне было интересно, всегда ли мне включать эту -ffloat-storeопцию?

Поскольку протестированная мной версия g ++ поставляется с CentOS / Red Hat Linux 5 и CentOS / Redhat 6 .

Я скомпилировал многие из моих программ для этих платформ и опасаюсь, что это вызовет неожиданные ошибки в моих программах. Кажется немного сложным исследовать весь мой код C ++ и используемые библиотеки, есть ли у них такие проблемы. Любое предложение?

Кого-нибудь интересует, почему даже /fp:fastвключился Visual Studio 2008 до сих пор работает? Кажется, Visual Studio 2008 более надежен в этой проблеме, чем g ++?


51
Всем новым пользователям SO: вот как вы задаете вопрос. +1
tenfour

1
FWIW, я получаю правильный результат с g ++ 4.5.0 с использованием MinGW.
Стив Блэквелл,

2
ideone использует 4.3.4 ideone.com/b8VXg
Дэниел А. Уайт

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

2
Тем, кто не может воспроизвести ошибку: не раскомментируйте закомментированные строки отладки, они влияют на результат.
п. 'местоимения' м.

Ответы:


91

Процессоры Intel x86 внутренне используют 80-битную расширенную точность, тогда doubleкак обычно имеют 64-битную ширину. Различные уровни оптимизации влияют на то, как часто значения с плавающей запятой из ЦП сохраняются в памяти и, таким образом, округляются от 80-битной до 64-битной точности.

Используйте параметр -ffloat-storegcc, чтобы получить одинаковые результаты с плавающей запятой с разными уровнями оптимизации.

В качестве альтернативы используйте long doubleтип, который обычно имеет ширину 80-бит в gcc, чтобы избежать округления с 80-битной до 64-битной точности.

man gcc говорит все:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

В сборках x86_64 компиляторы используют регистры SSE для floatи doubleпо умолчанию, поэтому не используется расширенная точность и эта проблема не возникает.

gccПараметр компилятора-mfpmath управляет этим.


20
Думаю, это ответ. Константа 4.55 преобразуется в 4.54999999999999, которое является ближайшим двоичным представлением в 64 бита; умножьте на 10 и снова округлите до 64 бит, и вы получите 45,5. Если вы пропустите этап округления, сохранив его в 80-битном регистре, вы получите 45,4999999999999.
Марк Рэнсом,

Спасибо, даже не знаю этот вариант. Но мне было интересно, всегда ли мне включать параметр -ffloat-store? Поскольку протестированная мной версия g ++ поставляется с CentOS / Redhat 5 и CentOS / Redhat 6. Я скомпилировал многие свои программы на этих платформах, я беспокоюсь, что это вызовет неожиданные ошибки в моих программах.
Медведь,

5
@Bear, оператор отладки, вероятно, вызывает сброс значения из регистра в память.
Марк Рэнсом,

2
@Bear, обычно ваше приложение должно получать выгоду от повышенной точности, если только оно не работает с очень маленькими или огромными значениями, когда ожидается, что 64-битное число с плавающей запятой будет недостаточным или переполненным и выдаст inf. Нет хорошего эмпирического правила, модульные тесты могут дать вам однозначный ответ.
Максим Егорушкин

2
@bear Как правило, если вам нужны совершенно предсказуемые результаты и / или именно то, что человек получит, делая суммы на бумаге, вам следует избегать операций с плавающей запятой. -ffloat-store устраняет один источник непредсказуемости, но это не волшебная пуля.
plugwash

10

Результат должен быть: 4.5 4.6 Вот каким был бы результат, если бы у вас была бесконечная точность или если бы вы работали с устройством, которое использовало десятичное, а не двоичное представление с плавающей запятой. Но это не так. Большинство компьютеров используют двоичный стандарт с плавающей запятой IEEE.

Как уже отмечал Максим Егорушкин в своем ответе, часть проблемы заключается в том, что внутри вашего компьютера используется 80-битное представление с плавающей запятой. Но это лишь часть проблемы. В основе проблемы лежит то, что любое число вида n.nn5 не имеет точного двоичного плавающего представления. Эти угловые случаи всегда являются неточными числами.

Если вы действительно хотите, чтобы ваше округление могло надежно обходить эти угловые случаи, вам нужен алгоритм округления, учитывающий тот факт, что n.n5, n.nn5 или n.nnn5 и т. Д. (Но не n.5) всегда неточный. Найдите угловой регистр, который определяет, округляется ли какое-либо входное значение в большую или меньшую сторону, и возвращает округленное значение в большую или меньшую сторону на основе сравнения с этим угловым случаем. И вам нужно позаботиться о том, чтобы оптимизирующий компилятор не поместил найденный угловой регистр в регистр расширенной точности.

Посмотрите, как Excel успешно округляет плавающие числа, даже если они неточные? для такого алгоритма.

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


6

У разных компиляторов разные настройки оптимизации. Некоторые из этих быстрых настроек оптимизации не поддерживают строгие правила с плавающей запятой в соответствии с IEEE 754 . Visual Studio имеет настройки конкретных, /fp:strict, /fp:precise, /fp:fast, где /fp:fastнарушает стандарт на то , что может быть сделано. Вы можете обнаружить, что именно этот флаг управляет оптимизацией в таких настройках. Вы также можете найти аналогичный параметр в GCC, который меняет поведение.

Если это так, то единственное, что различается между компиляторами, - это то, что GCC будет искать самое быстрое поведение с плавающей запятой по умолчанию при более высокой оптимизации, тогда как Visual Studio не изменяет поведение с плавающей запятой с более высокими уровнями оптимизации. Таким образом, это не обязательно может быть фактическая ошибка, а предполагаемое поведение параметра, о включении которого вы не знали.


4
Есть -ffast-mathпереключатель для GCC, который не включается ни на одном из -Oуровней оптимизации, начиная с цитаты: «это может привести к неправильному выводу программ, которые зависят от точной реализации правил / спецификаций IEEE или ISO для математических функций».
Mat,

@Mat: Я пробовал -ffast-mathи еще несколько вещей на моем, g++ 4.4.3но все еще не могу воспроизвести проблему.
NPE

Приятно: в обоих случаях -ffast-mathя получаю 4.5уровни оптимизации выше 0.
Kerrek SB

: (Correction я 4.5с -O1и -O2, но не -O0и -O3в GCC 4.4.3, но -O1,2,3в GCC 4.6.1.)
Kerrek SB

4

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

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

Дальнейший вопрос:

Мне было интересно, всегда ли мне включать -ffloat-storeопцию?

Чтобы быть легкомысленным, должна быть причина, по которой некоторые программисты не включаются -ffloat-store, иначе такой опции не существовало бы (аналогично, должна быть причина, по которой некоторые программисты действительно включаются -ffloat-store). Я бы не рекомендовал всегда включать или выключать его. Включение этого параметра предотвращает некоторые оптимизации, но отключение позволяет добиться того поведения, которое вы получаете.

Но, как правило, существует некоторое несоответствие между двоичными числами с плавающей запятой (как в компьютере) и десятичными числами с плавающей запятой (с которыми люди знакомы), и это несоответствие может привести к поведению, аналогичному тому, что вы получаете (для ясности, поведение вы получаете не вызвано этим несоответствием, но подобное поведение может быть). Дело в том, что, поскольку у вас уже есть некоторая неясность при работе с плавающей запятой, я не могу сказать, что -ffloat-storeэто делает его лучше или хуже.

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


Если вы не округляете для вывода, я бы, вероятно, посмотрел на std::modf()(in cmath) и std::numeric_limits<double>::epsilon()(in limits). Обдумывая исходную round()функцию, я считаю, что было бы чище заменить вызов на std::floor(d + .5)вызов этой функции:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Я думаю, это предполагает следующее улучшение:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Простое примечание: std::numeric_limits<T>::epsilon()определяется как «наименьшее число, добавленное к 1, которое дает число, не равное единице». Обычно вам нужно использовать относительный эпсилон (т.е. масштабировать эпсилон как-то, чтобы учесть тот факт, что вы работаете с числами, отличными от «1»). Сумма d, .5и std::numeric_limits<double>::epsilon()должна быть около 1, так что добавление группировки означает , что std::numeric_limits<double>::epsilon()будут нужного размера для того, что мы делаем. Во всяком случае, std::numeric_limits<double>::epsilon()будет слишком большим (когда сумма всех трех меньше единицы) и может заставить нас округлить некоторые числа в большую сторону, когда мы этого не должны.


В настоящее время вам следует подумать std::nearbyint().


«Относительный эпсилон» называется 1 ulp (1 единица на последнем месте). x - nextafter(x, INFINITY)относится к 1 ulp для x (но не используйте это; я уверен, что есть угловые случаи, и я только что придумал). В примере cppreference для epsilon() есть пример его масштабирования для получения относительной ошибки на основе ULP .
Питер Кордес

2
Кстати, в 2016 году ответ на этот вопрос -ffloat-storeтаков: вообще не используйте x87. Используйте математику SSE2 (64-разрядные двоичные файлы или -mfpmath=sse -msse2для создания старых 32-разрядных двоичных файлов), потому что SSE / SSE2 имеет временные файлы без дополнительной точности. doubleа floatпеременные в регистрах XMM действительно имеют 64-разрядный или 32-разрядный формат IEEE. (В отличие от x87, где регистры всегда 80-битные, а в памяти сохраняются до 32 или 64 бит.)
Питер Кордес,

3

Принятый ответ верен, если вы компилируете целевой объект x86, который не включает SSE2. Все современные процессоры x86 поддерживают SSE2, поэтому, если вы можете этим воспользоваться, вам следует:

-mfpmath=sse -msse2 -ffp-contract=off

Давайте разберемся с этим.

-mfpmath=sse -msse2. При этом выполняется округление с использованием регистров SSE2, что намного быстрее, чем сохранение каждого промежуточного результата в памяти. Обратите внимание, что это уже значение по умолчанию в GCC для x86-64. Из вики GCC :

На более современных процессорах x86, поддерживающих SSE2, указание параметров компилятора -mfpmath=sse -msse2гарантирует, что все операции с плавающей запятой и двойные операции выполняются в регистрах SSE и правильно округляются. Эти параметры не влияют на ABI и поэтому должны использоваться по возможности для предсказуемых численных результатов.

-ffp-contract=off. Однако контроля округления недостаточно для точного совпадения. Инструкции FMA (объединенное умножение-сложение) могут изменить поведение округления по сравнению с его неслитными аналогами, поэтому нам нужно отключить его. Это значение по умолчанию для Clang, а не для GCC. Как объясняется в этом ответе :

FMA имеет только одно округление (оно эффективно сохраняет бесконечную точность для внутреннего временного результата умножения), а ADD + MUL - два.

Отключив FMA, мы получаем результаты, которые точно совпадают при отладке и выпуске, за счет некоторой производительности (и точности). Мы по-прежнему можем воспользоваться другими преимуществами производительности SSE и AVX.


1

Я углубился в эту проблему и могу внести больше уточнений. Во-первых, точные представления 4.45 и 4.55 согласно gcc на x84_64 следующие (с libquadmath для вывода последней точности):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Как сказал Максим выше, проблема связана с размером регистров FPU 80 бит.

Но почему проблема никогда не возникает в Windows? на IA-32 FPU x87 был настроен на использование внутренней точности для мантиссы в 53 бита (что эквивалентно общему размеру 64 бита :) double. Для Linux и Mac OS использовалась точность по умолчанию в 64 бита (что эквивалентно общему размеру 80 бит :) long double. Таким образом, проблема должна быть возможна или нет на этих разных платформах путем изменения управляющего слова FPU (при условии, что последовательность инструкций вызовет ошибку). Об этой проблеме в gcc сообщили как об ошибке 323 (прочтите хотя бы комментарий 92!).

Чтобы показать точность мантиссы в Windows, вы можете скомпилировать это в 32 бита с помощью VC ++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

и в Linux / Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Обратите внимание, что с помощью gcc вы можете установить точность FPU -mpc32/64/80, хотя в Cygwin она игнорируется. Но имейте в виду, что это изменит размер мантиссы, но не экспоненты, что позволит открыть дверь для других видов различного поведения.

В архитектуре x86_64 SSE используется, как сказано в tmandry , поэтому проблема не возникнет, если вы не заставите старый FPU x87 для вычислений FP -mfpmath=387или если вы не скомпилируете в 32-битном режиме с -m32(вам понадобится Multilib package). Я мог воспроизвести проблему в Linux с различными комбинациями флагов и версий gcc:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Я пробовал несколько комбинаций в Windows или Cygwin с VC ++ / gcc / tcc, но ошибка так и не обнаружилась. Я полагаю, что последовательность сгенерированных инструкций не та.

Наконец, обратите внимание, что экзотическим способом предотвратить эту проблему с 4.45 или 4.55 было бы использование _Decimal32/64/128, но поддержки действительно мало ... Я потратил много времени только на то, чтобы иметь возможность сделать printf с libdfp!


0

Лично я столкнулся с той же проблемой, идя другим путем - от gcc к VS. В большинстве случаев я считаю, что оптимизации лучше избегать. Единственный раз, когда это имеет смысл, - это когда вы имеете дело с численными методами, включающими большие массивы данных с плавающей запятой. Даже после дизассемблирования я часто не в восторге от выбора компиляторов. Очень часто проще использовать встроенные функции компилятора или просто написать сборку самостоятельно.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.