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

Приведенный ниже код работает в 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 ++?


person Bear    schedule 22.09.2011    source источник
comment
Всем новым пользователям SO: вот как вы задаете вопрос. +1   -  person tenfour    schedule 22.09.2011
comment
FWIW, я получаю правильный результат с g ++ 4.5.0 с использованием MinGW.   -  person Steve Blackwell    schedule 22.09.2011
comment
Получаю 4.5 4.6 для всех случаев. Какая у вас версия g ++? У меня g ++ (Debian 4.3.2-1.1) 4.3.2   -  person ott--    schedule 22.09.2011
comment
@Bear: я не могу воспроизвести это с помощью GCC 4.5.3 или 4.6.1 на x86 или x86_64. (Но я понятия не имею, ошибка ли это или просто одна из многих неприятностей с плавающей запятой.)   -  person Mat    schedule 22.09.2011
comment
Это работает должным образом с GCC 4.4.3 и 4.6.1 на x86 с или без -std=c++0x и с любыми из -O0, -O1, -O2 и -O3.   -  person Kerrek SB    schedule 22.09.2011
comment
Я не могу воспроизвести это с g++ 4.4.3 в 64-битной Ubuntu 10.04. Это распечатает 4.5 4.6 с _3 _.._ 4_ и без него.   -  person NPE    schedule 22.09.2011
comment
К вашему сведению: gcc 4.6.1 (20110409) дает правильные результаты в Linux.   -  person    schedule 22.09.2011
comment
g ++ 4.6.1, результаты верны с оптимизацией и без нее.   -  person Didier Trosset    schedule 22.09.2011
comment
На g ++ (Ubuntu / Linaro 4.5.2-8ubuntu4) 4.5.2 я получаю правильный результат (g++ -O3 x.cpp, то же самое для -O[012])   -  person Lekensteyn    schedule 22.09.2011
comment
Интересно, связано ли это с этим: gcc.gnu.org/gcc-4.5 /changes.html GCC интегрирован с библиотекой MPC. Это позволяет GCC более точно оценивать сложную арифметику во время компиляции.   -  person Daniel A. White    schedule 22.09.2011
comment
ideone использует 4.3.4 ideone.com/b8VXg   -  person Daniel A. White    schedule 22.09.2011
comment
Вы должны иметь в виду, что ваш распорядок вряд ли будет надежно работать с любым типом вывода. В отличие от округления двойного до целого числа, это уязвимо из-за того, что не все действительные числа могут быть представлены, поэтому вы должны ожидать появления большего количества ошибок, подобных этой.   -  person Jakub Wieczorek    schedule 22.09.2011
comment
Тем, кто не может воспроизвести ошибку: не раскомментируйте закомментированные строки отладки, они влияют на результат.   -  person n. 1.8e9-where's-my-share m.    schedule 22.09.2011
comment
Невозможно воспроизвести на gcc-4.5.1.   -  person Maxim Egorushkin    schedule 22.09.2011
comment
Как ни странно, в 2006 году я столкнулся с подобной проблемой. Затем меня направили к следующему отчету об ошибке: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323.   -  person Throwback1986    schedule 22.09.2011
comment
Я мог бы воспроизвести его на x86_64 Linux с правильными флагами, см. Ответ ниже.   -  person calandoa    schedule 15.06.2018


Ответы (7)


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

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

В качестве альтернативы используйте тип 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 параметр компилятора _9 _ контролирует это.

person Maxim Egorushkin    schedule 22.09.2011
comment
Думаю, это ответ. Константа 4.55 преобразуется в 4.54999999999999, которое является ближайшим двоичным представлением в 64 бита; умножьте на 10 и снова округлите до 64 бит, и вы получите 45,5. Если вы пропустите шаг округления, сохранив его в 80-битном регистре, вы получите 45,4999999999999. - person Mark Ransom; 22.09.2011
comment
Спасибо, даже не знаю этот вариант. Но мне было интересно, всегда ли мне включать -ffloat-store? Поскольку версия g ++, которую я тестировал, поставляется с CentOS / Redhat 5 и CentOS / Redhat 6. Я скомпилировал много своих программ на этих платформах, меня беспокоит, что это вызовет неожиданные ошибки в моих программах. - person Bear; 22.09.2011
comment
@Mark: Но как объяснить, если вы раскомментируете отладочную инструкцию: std :: cout ‹---------------- t: ‹< t ‹< std :: endl; вывод будет правильный ??? - person Bear; 22.09.2011
comment
@Bear, оператор отладки, вероятно, вызывает сброс значения из регистра в память. - person Mark Ransom; 22.09.2011
comment
@Bear, обычно ваше приложение должно получать выгоду от повышенной точности, если только оно не работает с очень маленькими или огромными значениями, когда ожидается, что 64-битное число с плавающей запятой будет недостаточным или переполненным и выдаст inf. Нет хорошего практического правила, модульные тесты могут дать вам однозначный ответ. - person Maxim Egorushkin; 22.09.2011
comment
@bear Как правило, если вам нужны совершенно предсказуемые результаты и / или именно то, что человек получил бы, делая суммы на бумаге, вам следует избегать операций с плавающей запятой. -ffloat-store устраняет один источник непредсказуемости, но это не волшебная пуля. - person plugwash; 25.11.2015
comment
@plugwash Это правило не поможет, если вам нужно использовать числа с плавающей запятой. - person Maxim Egorushkin; 25.11.2015

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

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

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

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

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

person David Hammen    schedule 22.09.2011

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

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

person Puppy    schedule 22.09.2011
comment
Для GCC есть переключатель -ffast-math, который не включается ни одним из -O уровней оптимизации, начиная с цитаты: это может привести к неправильному выводу для программ, которые зависят от точной реализации правил / спецификаций IEEE или ISO для математических функций. - person Mat; 22.09.2011
comment
@Mat: Я пробовал -ffast-math и еще кое-что на моем g++ 4.4.3, но все еще не могу воспроизвести проблему. - person NPE; 22.09.2011
comment
Приятно: с -ffast-math я получаю 4.5 в обоих случаях для уровней оптимизации выше 0. - person Kerrek SB; 22.09.2011
comment
(Исправление: я получаю 4.5 с -O1 и -O2, но не с -O0 и -O3 в GCC 4.4.3, а с -O1,2,3 в GCC 4.6.1.) - person Kerrek SB; 22.09.2011

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

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

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

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

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

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

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


Если вы не округляете для вывода, я бы, вероятно, посмотрел на std::modf()cmath) и std::numeric_limits<double>::epsilon()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. Обычно вам нужно использовать относительный эпсилон (т. Е. Масштабировать эпсилон так или иначе, чтобы учесть тот факт, что вы работаете с числами, отличными от 1). чем 1). Сумма d, .5 и std::numeric_limits<double>::epsilon() должна быть близка к 1, поэтому группировка этого сложения означает, что std::numeric_limits<double>::epsilon() будет примерно подходящего размера для того, что мы делаем. Во всяком случае, std::numeric_limits<double>::epsilon() будет слишком большим (когда сумма всех трех меньше единицы) и может заставить нас округлить некоторые числа в большую сторону, когда мы этого не должны.


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

person Max Lybbert    schedule 22.09.2011
comment
Относительный эпсилон называется 1 ulp (1 единица на последнем месте). x - nextafter(x, INFINITY) относится к 1 ulp для x (но не используйте это; я уверен, что есть крайние случаи, и я только что придумал). В примере cppreference для epsilon() есть пример масштабирования, чтобы получить относительную величину на основе ULP ошибка. - person Peter Cordes; 01.11.2016
comment
Кстати, ответ на -ffloat-store в 2016 году: вообще не используйте x87. Используйте математику SSE2 (64-разрядные двоичные файлы или -mfpmath=sse -msse2 для создания старых 32-разрядных двоичных файлов), потому что SSE / SSE2 имеет временные файлы без дополнительной точности. Переменные double и float в регистрах XMM действительно имеют 64-разрядный или 32-разрядный формат IEEE. (В отличие от x87, где регистры всегда 80-битные, а сохранение в памяти округляется до 32 или 64 бит.) - person Peter Cordes; 01.11.2016

Принятый ответ верен, если вы компилируете целевой объект 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.

person tmandry    schedule 14.03.2018

Я углубился в эту проблему и могу внести больше уточнений. Во-первых, точные представления 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-пакет). Я мог воспроизвести проблему в 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!

person calandoa    schedule 15.06.2018

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

person cdcdcd    schedule 01.11.2016