Почему floor() так медленно?

Недавно я написал код (ISO/ANSI C), и был удивлен низкой результативностью, которую он достиг. Короче говоря, оказалось, что виновником была функция floor(). Он не только медленный, но и не векторизованный (с компилятором Intel, иначе ICL).

Вот несколько эталонных тестов для выполнения пол для всех ячеек в 2D-матрице:

VC:  0.10
ICL: 0.20

Сравните это с простым произведением:

VC:  0.04
ICL: 0.04

Как floor() может быть намного медленнее, чем просто? Это по сути то же самое (кроме отрицательных чисел). Второй вопрос: знает ли кто-нибудь о сверхбыстрой реализации floor()?

PS: Вот цикл, который я сравнивал:

void Floor(float *matA, int *intA, const int height, const int width, const int width_aligned)
{
    float *rowA=NULL;
    int   *intRowA=NULL;
    int   row, col;

    for(row=0 ; row<height ; ++row){
        rowA = matA + row*width_aligned;
        intRowA = intA + row*width_aligned;
#pragma ivdep
        for(col=0 ; col<width; ++col){
            /*intRowA[col] = floor(rowA[col]);*/
            intRowA[col] = (int)(rowA[col]);
        }
    }
}

Ответ 1

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

Самый важный:

floor может изменять глобальное состояние. Если вы передадите значение, которое должно быть огромным, чтобы быть представленным как целое число в формате float, переменная errno получает значение EDOM. Также выполняется специальная обработка для NaN. Все это поведение для приложений, которые хотят обнаружить случай переполнения и как-то обрабатывать ситуацию (не спрашивайте меня).

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

Некоторые компиляторы имеют специальные флаги компилятора, которые позволяют компилятору оптимизировать некоторые из редко используемых правил c-стандарта. Например, GCC можно сказать, что вы вообще не интересуетесь ошибкой. Для этого передайте -fno-math-errno или -ffast-math. ICC и VC могут иметь аналогичные флаги компилятора.

Btw - Вы можете катить свою собственную напольную функцию с помощью простых трансляций. Вам просто нужно обращаться с отрицательными и положительными случаями по-разному. Это может быть намного быстрее, если вам не нужна специальная обработка переполнений и NaN.

Ответ 2

Если вы собираетесь преобразовать результат операции floor() в int, и если вас не беспокоит переполнение, следующий код намного быстрее, чем (int)floor(x):

inline int int_floor(double x)
{
  int i = (int)x; /* truncate */
  return i - ( i > x ); /* convert trunc to floor */
}

Ответ 3

Безпоточный пол и потолок (лучше использовать пипилин) без проверки ошибок

int f(double x)
{
    return (int) x - (x < (int) x); // as dgobbi above, needs less than for floor
}

int c(double x)
{
    return (int) x + (x > (int) x);
}

или используя пол

int c(double x)
{
    return -(f(-x));
}

Ответ 4

Да, floor() очень медленный на всех платформах, поскольку он должен реализовывать множество действий из спецификации IEEE fp. Вы не можете использовать его во внутренних циклах.

Я иногда использую макрос для приближения к полу():

#define PSEUDO_FLOOR( V ) ((V) >= 0 ? (int)(V) : (int)((V) - 1))

Он не ведет себя точно как floor(): например, floor(-1) == -1, но PSEUDO_FLOOR(-1) == -2, но он достаточно близко для большинства целей.

Ответ 5

  • Они не делают то же самое. floor() - это функция. Поэтому использование этого метода вызывает вызов функции, распределение кадра стека, копирование параметров и получение результата. Кастинг не является вызовом функции, поэтому он использует более быстрые механизмы (я считаю, что он может использовать регистры для обработки значений).
  • Вероятно, floor() уже оптимизирован.
  • Можете ли вы выжать больше производительности из своего алгоритма? Может быть, могут помочь переключения строк и столбцов? Можете ли вы кэшировать общие значения? Включены ли все ваши оптимизации компилятора? Можете ли вы переключить операционную систему? компилятор? Jon Bentley Programming Pearls имеет отличный обзор возможных оптимизаций.

Ответ 6

Быстрый двойной раунд

double round(double x)
{
    return double((x>=0.5)?(int(x)+1):int(x));
}

Журнал терминалов

test custom_1 8.3837

test native_1 18.4989

test custom_2 8.36333

test native_2 18.5001

test custom_3 8.37316

test native_3 18.5012


Test

void test(char* name, double (*f)(double))
{
    int it = std::numeric_limits<int>::max();

    clock_t begin = clock();

    for(int i=0; i<it; i++)
    {
        f(double(i)/1000.0);
    }
    clock_t end = clock();

    cout << "test " << name << " " << double(end - begin) / CLOCKS_PER_SEC << endl;

}

int main(int argc, char **argv)
{

    test("custom_1",round);
    test("native_1",std::round);
    test("custom_2",round);
    test("native_2",std::round);
    test("custom_3",round);
    test("native_3",std::round);
    return 0;
}

Результат

Тип каста и использование вашего мозга в 3 раза быстрее, чем использование собственных функций.