Почему 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 раза быстрее, чем использование собственных функций.