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

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

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

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

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

Ответы [ 7 ]

86 голосов
/ 22 сентября 2011

Процессоры 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.
10 голосов
/ 22 сентября 2011

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

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

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

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

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

6 голосов
/ 22 сентября 2011

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

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

4 голосов
/ 22 сентября 2011

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

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


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

1 голос
/ 15 июня 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

Как сказано выше Максим , проблема связана с размером 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 , поэтому проблема не возникнет, если вы не принудительно используете старый 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!

1 голос
/ 14 марта 2018

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

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

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

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

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

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

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

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

0 голосов
/ 01 ноября 2016

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

...