round () для поплавка в C ++ - PullRequest
       123

round () для поплавка в C ++

225 голосов
/ 28 января 2009

Мне нужна простая функция округления с плавающей запятой, таким образом:

double round(double);

round(0.1) = 0
round(-0.1) = 0
round(-0.9) = -1

Я могу найти ceil() и floor() в math.h - но не round().

Он присутствует в стандартной библиотеке C ++ под другим именем или отсутствует ??

Ответы [ 20 ]

4 голосов
/ 18 июня 2012

Остерегайтесь floor(x+0.5). Вот что может произойти для нечетных чисел в диапазоне [2 ^ 52,2 ^ 53]:

-bash-3.2$ cat >test-round.c <<END

#include <math.h>
#include <stdio.h>

int main() {
    double x=5000000000000001.0;
    double y=round(x);
    double z=floor(x+0.5);
    printf("      x     =%f\n",x);
    printf("round(x)    =%f\n",y);
    printf("floor(x+0.5)=%f\n",z);
    return 0;
}
END

-bash-3.2$ gcc test-round.c
-bash-3.2$ ./a.out
      x     =5000000000000001.000000
round(x)    =5000000000000001.000000
floor(x+0.5)=5000000000000002.000000

Это http://bugs.squeak.org/view.php?id=7134. Используйте решение, подобное @ konik.

Моя надежная версия будет выглядеть примерно так:

double round(double x)
{
    double truncated,roundedFraction;
    double fraction = modf(x, &truncated);
    modf(2.0*fraction, &roundedFraction);
    return truncated + roundedFraction;
}

Другая причина, по которой следует избегать пола (х + 0,5), приведена здесь .

3 голосов
/ 17 ноября 2017

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

  • std::nearbyint()

    • x86 clang: вставляет один insn с -msse4.1.
    • x86 gcc: встраивает в один insn только с -msse4.1 -ffast-math и только на gcc 5.4 и ранее . Позже gcc никогда не указывает на это (возможно, они не понимали, что один из непосредственных битов может подавить неточное исключение? Это то, что использует clang, но более старый gcc использует тот же самый непосредственный, что и для rint, когда он встроен)
    • AArch64 gcc6.3: по умолчанию используется один insn.
  • std::rint

    • x86 clang: вставляет один insn с -msse4.1
    • x86 gcc7: вставляет один insn с -msse4.1. (Без SSE4.1, содержит несколько инструкций)
    • x86 gcc6.x и более ранних версий: вставляет один insn с -ffast-math -msse4.1.
    • AArch64 gcc: по умолчанию используется один insn
  • std::round

    • x86 clang: не встроен
    • x86 gcc: встраивает в несколько команд с -ffast-math -msse4.1, требуя двух векторных констант.
    • AArch64 gcc: вставляет одну инструкцию (HW поддерживает этот режим округления, а также стандарт IEEE и большинство других.)
  • std::floor / std::ceil / std::trunc

    • x86 clang: вставляет один insn с -msse4.1
    • x86 gcc7.x: вставляет один insn с -msse4.1
    • x86 gcc6.x и более ранние: вставляет один insn с -ffast-math -msse4.1
    • AArch64 gcc: по умолчанию вставляет одну инструкцию

Округление до 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, не очень медленные.
2 голосов
/ 09 февраля 2011

Функция double round(double) с использованием функции modf:

double round(double x)
{
    using namespace std;

    if ((numeric_limits<double>::max() - 0.5) <= x)
        return numeric_limits<double>::max();

    if ((-1*std::numeric_limits<double>::max() + 0.5) > x)
        return (-1*std::numeric_limits<double>::max());

    double intpart;
    double fractpart = modf(x, &intpart);

    if (fractpart >= 0.5)
        return (intpart + 1);
    else if (fractpart >= -0.5)
        return intpart;
    else
        return (intpart - 1);
    }

Чтобы быть скомпилированным, необходимо включить "math.h" и "limit". Функция работает в соответствии со следующей схемой округления:

  • раунд 5,0 - 5,0
  • раунд 3,8 - 4,0
  • раунд 2,3 - 2,0
  • раунд 1,5 - 2,0
  • раунд 0,501 - 1,0
  • раунд 0,5 - 1,0
  • раунд 0,499 - 0,0
  • раунд 0,01 - 0,0
  • раунд 0,0 - 0,0
  • раунд -0,01 -0,0
  • раунд -0,499 это -0,0
  • раунд -0,5 -0,0
  • раунд -0,501 -1,0
  • раунд -1,5 -1,0
  • раунд -2,3 - -2,0
  • раунд -3,8 -4,0
  • раунд от -5,0 до -5,0
2 голосов
/ 04 февраля 2017

Нет необходимости что-либо реализовывать, поэтому я не уверен, почему так много ответов связаны с определениями, функциями или методами.

В C99

У нас есть следующий и и заголовок для макросов общего типа.

#include <math.h>
double round (double x);
float roundf (float x);
long double roundl (long double x);

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

gcc -lm -std=c99 ...

В С ++ 11

У нас есть следующие и дополнительные перегрузки в #include , которые зависят от плавающей запятой двойной точности IEEE.

#include <math.h>
double round (double x);
float round (float x);
long double round (long double x);
double round (T x);

Также есть эквивалентов в пространстве имен std .

Если вы не можете скомпилировать это, возможно, вы используете компиляцию C вместо C ++. Следующая базовая команда не выдает ни ошибок, ни предупреждений с g ++ 6.3.1, x86_64-w64-mingw32-g ++ 6.3.0, clang-x86_64 ++ 3.8.0 и сообществом Visual C ++ 2015.

g++ -std=c++11 -Wall

с порядковым делением

При делении двух порядковых чисел, где T - это короткий, int, длинный или другой порядковый номер, выражение округления выглядит следующим образом.

T roundedQuotient = (2 * integerNumerator + 1)
    / (2 * integerDenominator);

Точность

Нет сомнений в том, что странные неточности появляются в операциях с плавающей запятой, но это только тогда, когда появляются числа и имеют мало общего с округлением.

Источником является не просто число значащих цифр в мантиссе представления IEEE числа с плавающей запятой, оно связано с нашим десятичным мышлением как людей.

Десять - это произведение пяти и двух, а 5 и 2 относительно простые. Поэтому стандарты IEEE с плавающей запятой не могут быть идеально представлены в виде десятичных чисел для всех двоичных цифровых представлений.

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

1 голос
/ 16 ноября 2017

Как отмечалось в комментариях и других ответах, стандартная библиотека ISO C ++ не добавляла round() до ISO C ++ 11, когда эта функция была задействована со ссылкой на стандартную математическую библиотеку ISO C99.

Для положительных операндов в [½, ub ] round(x) == floor (x + 0.5), где ub равно 2 23 для float при сопоставлении с IEEE-754 ( 2008) binary32 и 2 52 для double при сопоставлении с IEEE-754 (2008) binary64. Числа 23 и 52 соответствуют числу сохраненных битов мантиссы в этих двух форматах с плавающей запятой. Для положительных операндов в [+0, ½) round(x) == 0 и для положительных операндов в ( ub , + ∞] round(x) == x. Поскольку функция симметрична относительно оси x, отрицательные аргументы x может обрабатываться в соответствии с round(-x) == -round(x).

Это приводит к компактному коду ниже. Он компилируется в разумное количество машинных инструкций на разных платформах. Я наблюдал самый компактный код на графических процессорах, где my_roundf() требует около десятка инструкций. В зависимости от архитектуры процессора и цепочки инструментов этот подход на основе чисел с плавающей запятой может быть быстрее или медленнее, чем целочисленная реализация из newlib, на которую ссылается другой ответ .

Я полностью протестировал my_roundf() против реализации newlib roundf() с использованием компилятора Intel версии 13, с /fp:strict и /fp:fast. Я также проверил, что версия newlib соответствует roundf() в библиотеке mathimf компилятора Intel. Исчерпывающее тестирование невозможно для двойной точности round(), однако код структурно идентичен реализации с одинарной точностью.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

float my_roundf (float x)
{
    const float half = 0.5f;
    const float one = 2 * half;
    const float lbound = half;
    const float ubound = 1L << 23;
    float a, f, r, s, t;
    s = (x < 0) ? (-one) : one;
    a = x * s;
    t = (a < lbound) ? x : s;
    f = (a < lbound) ? 0 : floorf (a + half);
    r = (a > ubound) ? x : (t * f);
    return r;
}

double my_round (double x)
{
    const double half = 0.5;
    const double one = 2 * half;
    const double lbound = half;
    const double ubound = 1ULL << 52;
    double a, f, r, s, t;
    s = (x < 0) ? (-one) : one;
    a = x * s;
    t = (a < lbound) ? x : s;
    f = (a < lbound) ? 0 : floor (a + half);
    r = (a > ubound) ? x : (t * f);
    return r;
}

uint32_t float_as_uint (float a)
{
    uint32_t r;
    memcpy (&r, &a, sizeof(r));
    return r;
}

float uint_as_float (uint32_t a)
{
    float r;
    memcpy (&r, &a, sizeof(r));
    return r;
}

float newlib_roundf (float x)
{
    uint32_t w;
    int exponent_less_127;

    w = float_as_uint(x);
    /* Extract exponent field. */
    exponent_less_127 = (int)((w & 0x7f800000) >> 23) - 127;
    if (exponent_less_127 < 23) {
        if (exponent_less_127 < 0) {
            /* Extract sign bit. */
            w &= 0x80000000;
            if (exponent_less_127 == -1) {
                /* Result is +1.0 or -1.0. */
                w |= ((uint32_t)127 << 23);
            }
        } else {
            uint32_t exponent_mask = 0x007fffff >> exponent_less_127;
            if ((w & exponent_mask) == 0) {
                /* x has an integral value. */
                return x;
            }
            w += 0x00400000 >> exponent_less_127;
            w &= ~exponent_mask;
        }
    } else {
        if (exponent_less_127 == 128) {
            /* x is NaN or infinite so raise FE_INVALID by adding */
            return x + x;
        } else {
            return x;
        }
    }
    x = uint_as_float (w);
    return x;
}

int main (void)
{
    uint32_t argi, resi, refi;
    float arg, res, ref;

    argi = 0;
    do {
        arg = uint_as_float (argi);
        ref = newlib_roundf (arg);
        res = my_roundf (arg);
        resi = float_as_uint (res);
        refi = float_as_uint (ref);
        if (resi != refi) { // check for identical bit pattern
            printf ("!!!! arg=%08x  res=%08x  ref=%08x\n", argi, resi, refi);
            return EXIT_FAILURE;
        }
        argi++;
    } while (argi);
    return EXIT_SUCCESS;
}
1 голос
/ 16 декабря 2015

Если вам нужно иметь возможность компилировать код в средах, которые поддерживают стандарт C ++ 11, но также необходимо иметь возможность компилировать этот же код в средах, которые его не поддерживают, вы можете использовать макрос функции для выберите между std :: round () и пользовательской функцией для каждой системы. Просто передайте -DCPP11 или /DCPP11 компилятору, совместимому с C ++ 11 (или используйте встроенные макросы версии), и создайте заголовок, подобный этому:

// File: rounding.h
#include <cmath>

#ifdef CPP11
    #define ROUND(x) std::round(x)
#else    /* CPP11 */
    inline double myRound(double x) {
        return (x >= 0.0 ? std::floor(x + 0.5) : std::ceil(x - 0.5));
    }

    #define ROUND(x) myRound(x)
#endif   /* CPP11 */

Для быстрого примера см. http://ideone.com/zal709.

Это приблизительно соответствует std :: round () в средах, не совместимых с C ++ 11, включая сохранение знакового бита для -0.0. Однако это может привести к незначительному снижению производительности и, вероятно, к проблемам с округлением определенных известных «проблемных» значений с плавающей запятой, таких как 0,49999999999999994 или аналогичных значений.

В качестве альтернативы, если у вас есть доступ к C ++ 11-совместимому компилятору, вы можете просто взять std :: round () из его заголовка <cmath> и использовать его для создания собственного заголовка, который определяет функцию, если она еще не определено. Обратите внимание, что это может быть не оптимальным решением, особенно, если вам нужно компилировать для нескольких платформ.

1 голос
/ 24 октября 2013

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

    // round a floating point number to the nearest integer
    template <typename Arg>
    int Round(Arg arg)
    {
#ifndef NDEBUG
        // check that the argument can be rounded given the return type:
        if (
            (Arg)std::numeric_limits<int>::max() < arg + (Arg) 0.5) ||
            (Arg)std::numeric_limits<int>::lowest() > arg - (Arg) 0.5)
            )
        {
            throw std::overflow_error("out of bounds");
        }
#endif

        return (arg > (Arg) 0.0) ? (int)(r + (Arg) 0.5) : (int)(r - (Arg) 0.5);
    }
0 голосов
/ 28 октября 2014

Я использую следующую реализацию раунда в asm для архитектуры x86 и MS VS, специфичную для C ++:

__forceinline int Round(const double v)
{
    int r;
    __asm
    {
        FLD     v
        FISTP   r
        FWAIT
    };
    return r;
}

UPD: вернуть двойное значение

__forceinline double dround(const double v)
{
    double r;
    __asm
    {
        FLD     v
        FRNDINT
        FSTP    r
        FWAIT
    };
    return r;
}

Выход:

dround(0.1): 0.000000000000000
dround(-0.1): -0.000000000000000
dround(0.9): 1.000000000000000
dround(-0.9): -1.000000000000000
dround(1.1): 1.000000000000000
dround(-1.1): -1.000000000000000
dround(0.49999999999999994): 0.000000000000000
dround(-0.49999999999999994): -0.000000000000000
dround(0.5): 0.000000000000000
dround(-0.5): -0.000000000000000
0 голосов
/ 09 мая 2009

Я сделал это:

#include <cmath.h>

using namespace std;

double roundh(double number, int place){

    /* place = decimal point. Putting in 0 will make it round to whole
                              number. putting in 1 will round to the
                              tenths digit.
    */

    number *= 10^place;
    int istack = (int)floor(number);
    int out = number-istack;
    if (out < 0.5){
        floor(number);
        number /= 10^place;
        return number;
    }
    if (out > 0.4) {
        ceil(number);
        number /= 10^place;
        return number;
    }
}
0 голосов
/ 29 июня 2010
// Convert the float to a string
// We might use stringstream, but it looks like it truncates the float to only
//5 decimal points (maybe that's what you want anyway =P)

float MyFloat = 5.11133333311111333;
float NewConvertedFloat = 0.0;
string FirstString = " ";
string SecondString = " ";
stringstream ss (stringstream::in | stringstream::out);
ss << MyFloat;
FirstString = ss.str();

// Take out how ever many decimal places you want
// (this is a string it includes the point)
SecondString = FirstString.substr(0,5);
//whatever precision decimal place you want

// Convert it back to a float
stringstream(SecondString) >> NewConvertedFloat;
cout << NewConvertedFloat;
system("pause");

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

...