В наши дни не должно быть проблемой использовать компилятор C ++ 11, который включает математическую библиотеку C99 / C ++ 11. Но тогда возникает вопрос: какую функцию округления вы выбираете?
C99 / C ++ 11 round()
зачастую не является необходимой вам функцией округления . Он использует режим фанки-округления, который округляется от 0 в качестве тай-брейка на полпути (+-xxx.5000
). Если вы действительно хотите этот режим округления или нацеливаетесь на реализацию C ++, где round()
быстрее, чем rint()
, то используйте его (или эмулируйте его поведение с одним из других ответов на этот вопрос, который принял его лицом к лицу ценить и тщательно воспроизвести это специфическое поведение округления.)
Округление
round()
отличается от стандартного IEEE754 округления до ближайшего режима даже с разрывом связи . Ближайший - даже избегает статистического смещения средней величины чисел, но смещает в сторону четных чисел.
Существует две функции округления математической библиотеки, которые используют текущий режим округления по умолчанию: std::nearbyint()
и std::rint()
, обе добавлены в C99 / C ++ 11, поэтому они доступны в любое время std::round()
. Разница лишь в том, что nearbyint
никогда не вызывает FE_INEXACT.
Предпочитают rint()
по соображениям производительности : gcc и clang легче встроить его, но gcc никогда не вставляет nearbyint()
(даже с -ffast-math
)
gcc / clang для x86-64 и AArch64
Я поместил некоторые тестовые функции в Проводник компилятора Мэтта Годболта , где вы можете увидеть исходный код + вывод asm (для нескольких компиляторов). Для получения дополнительной информации о чтении выходных данных компилятора см. этого Q & A и доклад Мэтта CppCon2017: «Что мой компилятор сделал для меня в последнее время? Откручивание крышки компилятора »,
В FP-коде это обычно большой выигрыш для встроенных маленьких функций. Особенно в не Windows, где стандартное соглашение о вызовах не имеет регистров, сохраняющих вызовы, поэтому компилятор не может хранить значения FP в регистрах XMM через call
. Таким образом, даже если вы не знаете asm, вы все равно можете легко увидеть, является ли это просто хвостовым вызовом библиотечной функции или он встроен в одну или две математические инструкции. Все, что связано с одной или двумя инструкциями, лучше, чем вызов функции (для этой конкретной задачи на x86 или ARM).
На x86 все, что встроено в SSE4.1 roundsd
, может автоматически векторизоваться с SSE4.1 roundpd
(или AVX vroundpd
). (FP-> целочисленные преобразования также доступны в упакованном виде SIMD, за исключением FP-> 64-битного целого числа, для которого требуется AVX512.)
Округление до int
/ long
/ long long
:
У вас есть два варианта: использовать lrint
(например, rint
, но возвращает long
или long long
для llrint
), или использовать функцию округления FP-> FP, а затем преобразовать в целочисленный тип нормальный способ (с усечением). Некоторые компиляторы оптимизируют один путь лучше, чем другой.
long l = lrint(x);
int i = (int)rint(x);
Обратите внимание, что int i = lrint(x)
сначала преобразует float
или double
-> long
, а затем усекает целое число до int
. Это имеет значение для целых чисел вне диапазона: неопределенное поведение в C ++, но четко определенное для инструкций x86 FP -> int (которые компилятор будет выдавать, если он не увидит UB во время компиляции во время постоянного распространения, тогда это разрешено создавать код, который ломается, если он выполняется).
В x86 преобразование целых чисел FP->, которое переполняет целое число, дает INT_MIN
или LLONG_MIN
(битовый шаблон 0x8000000
или 64-битный эквивалент, только с установленным битом знака). Intel называет это «целочисленным неопределенным» значением. (См. cvttsd2si
ввод вручную , инструкцию SSE2, которая преобразует (с усечением) скалярное двойное число в целое число со знаком. Доступно с 32-разрядным или 64-разрядным целочисленным назначением (только в 64-разрядном режиме) Также есть cvtsd2si
(конвертирование с текущим режимом округления), который мы хотели бы, чтобы компилятор испускал, но, к сожалению, gcc и clang не сделают этого без -ffast-math
.
Также помните, что FP в / из unsigned
int / long менее эффективен на x86 (без AVX512). Преобразование в 32-битный unsigned на 64-битной машине довольно дешево; просто конвертировать в 64-битную подпись и обрезать. Но в остальном это значительно медленнее.
x86 clang с / без -ffast-math -msse4.1
: (int/long)rint
встраивает в roundsd
/ cvttsd2si
. (пропустил оптимизацию до cvtsd2si
). lrint
вообще не встраивается.
x86 gcc6.x и более ранних версий без -ffast-math
: ни в одну строку не вставляются
- x86 gcc7 без
-ffast-math
: (int/long)rint
округляет и преобразует отдельно (с двумя полными инструкциями SSE4.1 включено, в противном случае с кучей кода, встроенного для rint
без roundsd
). lrint
не встраивается.
x86 gcc с -ffast-math
: все пути встроены в cvtsd2si
(оптимально) , SSE4.1 не требуется.
AArch64 gcc6.3 без -ffast-math
: (int/long)rint
содержит 2 инструкции. lrint
не встроено
- AArch64 gcc6.3 с
-ffast-math
: (int/long)rint
компилирует вызов lrint
. lrint
не встроен. Это может быть пропущенная оптимизация, если только две инструкции, которые мы получаем без -ffast-math
, не очень медленные.