Приведенный ниже код работает в 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 ++?
источник
Ответы:
Процессоры Intel x86 внутренне используют 80-битную расширенную точность, тогда
double
как обычно имеют 64-битную ширину. Различные уровни оптимизации влияют на то, как часто значения с плавающей запятой из ЦП сохраняются в памяти и, таким образом, округляются от 80-битной до 64-битной точности.Используйте параметр
-ffloat-store
gcc, чтобы получить одинаковые результаты с плавающей запятой с разными уровнями оптимизации.В качестве альтернативы используйте
long double
тип, который обычно имеет ширину 80-бит в gcc, чтобы избежать округления с 80-битной до 64-битной точности.man gcc
говорит все:В сборках x86_64 компиляторы используют регистры SSE для
float
иdouble
по умолчанию, поэтому не используется расширенная точность и эта проблема не возникает.gcc
Параметр компилятора-mfpmath
управляет этим.источник
inf
. Нет хорошего эмпирического правила, модульные тесты могут дать вам однозначный ответ.Как уже отмечал Максим Егорушкин в своем ответе, часть проблемы заключается в том, что внутри вашего компьютера используется 80-битное представление с плавающей запятой. Но это лишь часть проблемы. В основе проблемы лежит то, что любое число вида n.nn5 не имеет точного двоичного плавающего представления. Эти угловые случаи всегда являются неточными числами.
Если вы действительно хотите, чтобы ваше округление могло надежно обходить эти угловые случаи, вам нужен алгоритм округления, учитывающий тот факт, что n.n5, n.nn5 или n.nnn5 и т. Д. (Но не n.5) всегда неточный. Найдите угловой регистр, который определяет, округляется ли какое-либо входное значение в большую или меньшую сторону, и возвращает округленное значение в большую или меньшую сторону на основе сравнения с этим угловым случаем. И вам нужно позаботиться о том, чтобы оптимизирующий компилятор не поместил найденный угловой регистр в регистр расширенной точности.
Посмотрите, как Excel успешно округляет плавающие числа, даже если они неточные? для такого алгоритма.
Или вы можете просто смириться с тем, что угловые корпуса иногда ошибочно закругляются.
источник
У разных компиляторов разные настройки оптимизации. Некоторые из этих быстрых настроек оптимизации не поддерживают строгие правила с плавающей запятой в соответствии с IEEE 754 . Visual Studio имеет настройки конкретных,
/fp:strict
,/fp:precise
,/fp:fast
, где/fp:fast
нарушает стандарт на то , что может быть сделано. Вы можете обнаружить, что именно этот флаг управляет оптимизацией в таких настройках. Вы также можете найти аналогичный параметр в GCC, который меняет поведение.Если это так, то единственное, что различается между компиляторами, - это то, что GCC будет искать самое быстрое поведение с плавающей запятой по умолчанию при более высокой оптимизации, тогда как Visual Studio не изменяет поведение с плавающей запятой с более высокими уровнями оптимизации. Таким образом, это не обязательно может быть фактическая ошибка, а предполагаемое поведение параметра, о включении которого вы не знали.
источник
-ffast-math
переключатель для GCC, который не включается ни на одном из-O
уровней оптимизации, начиная с цитаты: «это может привести к неправильному выводу программ, которые зависят от точной реализации правил / спецификаций IEEE или ISO для математических функций».-ffast-math
и еще несколько вещей на моем,g++ 4.4.3
но все еще не могу воспроизвести проблему.-ffast-math
я получаю4.5
уровни оптимизации выше0
.4.5
с-O1
и-O2
, но не-O0
и-O3
в GCC 4.4.3, но-O1,2,3
в GCC 4.6.1.)Это означает, что проблема связана с операторами отладки. И похоже, что есть ошибка округления, вызванная загрузкой значений в регистры во время операторов вывода, поэтому другие обнаружили, что вы можете исправить это с помощью
-ffloat-store
Чтобы быть легкомысленным, должна быть причина, по которой некоторые программисты не включаются
-ffloat-store
, иначе такой опции не существовало бы (аналогично, должна быть причина, по которой некоторые программисты действительно включаются-ffloat-store
). Я бы не рекомендовал всегда включать или выключать его. Включение этого параметра предотвращает некоторые оптимизации, но отключение позволяет добиться того поведения, которое вы получаете.Но, как правило, существует некоторое несоответствие между двоичными числами с плавающей запятой (как в компьютере) и десятичными числами с плавающей запятой (с которыми люди знакомы), и это несоответствие может привести к поведению, аналогичному тому, что вы получаете (для ясности, поведение вы получаете не вызвано этим несоответствием, но подобное поведение может быть). Дело в том, что, поскольку у вас уже есть некоторая неясность при работе с плавающей запятой, я не могу сказать, что
-ffloat-store
это делает его лучше или хуже.Вместо этого вы можете найти другие решения проблемы, которую пытаетесь решить (к сожалению, Кениг не указывает на настоящую статью, и я не могу найти для нее очевидного "канонического" места, поэтому я Придется вам в гугл отправить ).
Если вы не округляете для вывода, я бы, вероятно, посмотрел на
std::modf()
(incmath
) иstd::numeric_limits<double>::epsilon()
(inlimits
). Обдумывая исходнуюround()
функцию, я считаю, что было бы чище заменить вызов наstd::floor(d + .5)
вызов этой функции:Я думаю, это предполагает следующее улучшение:
Простое примечание:
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()
.источник
x - nextafter(x, INFINITY)
относится к 1 ulp для x (но не используйте это; я уверен, что есть угловые случаи, и я только что придумал). В примере cppreference дляepsilon()
есть пример его масштабирования для получения относительной ошибки на основе ULP .-ffloat-store
таков: вообще не используйте x87. Используйте математику SSE2 (64-разрядные двоичные файлы или-mfpmath=sse -msse2
для создания старых 32-разрядных двоичных файлов), потому что SSE / SSE2 имеет временные файлы без дополнительной точности.double
аfloat
переменные в регистрах XMM действительно имеют 64-разрядный или 32-разрядный формат IEEE. (В отличие от x87, где регистры всегда 80-битные, а в памяти сохраняются до 32 или 64 бит.)Принятый ответ верен, если вы компилируете целевой объект x86, который не включает SSE2. Все современные процессоры x86 поддерживают SSE2, поэтому, если вы можете этим воспользоваться, вам следует:
Давайте разберемся с этим.
-mfpmath=sse -msse2
. При этом выполняется округление с использованием регистров SSE2, что намного быстрее, чем сохранение каждого промежуточного результата в памяти. Обратите внимание, что это уже значение по умолчанию в GCC для x86-64. Из вики GCC :-ffp-contract=off
. Однако контроля округления недостаточно для точного совпадения. Инструкции FMA (объединенное умножение-сложение) могут изменить поведение округления по сравнению с его неслитными аналогами, поэтому нам нужно отключить его. Это значение по умолчанию для Clang, а не для GCC. Как объясняется в этом ответе :Отключив FMA, мы получаем результаты, которые точно совпадают при отладке и выпуске, за счет некоторой производительности (и точности). Мы по-прежнему можем воспользоваться другими преимуществами производительности SSE и AVX.
источник
Я углубился в эту проблему и могу внести больше уточнений. Во-первых, точные представления 4.45 и 4.55 согласно gcc на x84_64 следующие (с libquadmath для вывода последней точности):
Как сказал Максим выше, проблема связана с размером регистров FPU 80 бит.
Но почему проблема никогда не возникает в Windows? на IA-32 FPU x87 был настроен на использование внутренней точности для мантиссы в 53 бита (что эквивалентно общему размеру 64 бита :)
double
. Для Linux и Mac OS использовалась точность по умолчанию в 64 бита (что эквивалентно общему размеру 80 бит :)long double
. Таким образом, проблема должна быть возможна или нет на этих разных платформах путем изменения управляющего слова FPU (при условии, что последовательность инструкций вызовет ошибку). Об этой проблеме в gcc сообщили как об ошибке 323 (прочтите хотя бы комментарий 92!).Чтобы показать точность мантиссы в Windows, вы можете скомпилировать это в 32 бита с помощью VC ++:
и в Linux / Cygwin:
Обратите внимание, что с помощью gcc вы можете установить точность FPU
-mpc32/64/80
, хотя в Cygwin она игнорируется. Но имейте в виду, что это изменит размер мантиссы, но не экспоненты, что позволит открыть дверь для других видов различного поведения.В архитектуре x86_64 SSE используется, как сказано в tmandry , поэтому проблема не возникнет, если вы не заставите старый FPU x87 для вычислений FP
-mfpmath=387
или если вы не скомпилируете в 32-битном режиме с-m32
(вам понадобится Multilib package). Я мог воспроизвести проблему в Linux с различными комбинациями флагов и версий gcc:Я пробовал несколько комбинаций в Windows или Cygwin с VC ++ / gcc / tcc, но ошибка так и не обнаружилась. Я полагаю, что последовательность сгенерированных инструкций не та.
Наконец, обратите внимание, что экзотическим способом предотвратить эту проблему с 4.45 или 4.55 было бы использование
_Decimal32/64/128
, но поддержки действительно мало ... Я потратил много времени только на то, чтобы иметь возможность сделать printf сlibdfp
!источник
Лично я столкнулся с той же проблемой, идя другим путем - от gcc к VS. В большинстве случаев я считаю, что оптимизации лучше избегать. Единственный раз, когда это имеет смысл, - это когда вы имеете дело с численными методами, включающими большие массивы данных с плавающей запятой. Даже после дизассемблирования я часто не в восторге от выбора компиляторов. Очень часто проще использовать встроенные функции компилятора или просто написать сборку самостоятельно.
источник