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

приведенный ниже код работает в 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, результат по-прежнему правильный.

еще вопрос:

мне было интересно, должен ли я всегда включать ?

потому что версия g++, которую я тестировал, -поставляется с CentOS/Red Hat Linux 5 и CentOS / Redhat 6.

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

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

7 ответов


процессоры Intel x86 используют 80-битную расширенную точность внутри, тогда как double обычно 64-битные. Различные уровни оптимизации влияют на то, как часто значения с плавающей запятой из CPU сохраняются в памяти и, таким образом, округляются от 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.

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

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

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

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

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


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

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


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

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

еще вопрос:

мне было интересно, должен ли я всегда включать ?

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

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

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


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


в настоящее время, вы должны рассмотреть std::nearbyint().


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

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

давайте разберем это.

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

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

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

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

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


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


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

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

но почему проблема никогда не возникает в 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, так что проблема не произойдет, если вы не заставите старый x87 FPU для вычисления 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 !