#c #function #parameters #unions #absolute-value
#c #функция #параметры #объединения #абсолютное значение
Вопрос:
Приведенная ниже функция вычисляет абсолютное значение 32-разрядного значения с плавающей запятой:
__forceinline static float Abs(float x)
{
union {
float x;
int a;
} u;
//u.x = x;
u.a amp;= 0x7FFFFFFF;
return u.x;
}
объединение u, объявленное в функции, содержит переменную x, которая отличается от x, которая передается в качестве параметра в функции. Есть ли какой-либо способ создать объединение с аргументом для функции — x?
Есть ли причина, по которой функция выше с раскомментированной строкой выполняется дольше, чем эта?
__forceinline float fastAbs(float a)
{
int b= *((int *)amp;a) amp; 0x7FFFFFFF;
return *((float *)(amp;b));
}
Я пытаюсь найти наилучший способ получить значение Abs с плавающей запятой за как можно меньшее количество операций чтения / записи в память.
Комментарии:
1. Сколько времени
std::abs
занимает относительно вашей версии?2. кстати… почему бы не использовать std::fabs? поскольку для этого будет использоваться соответствующий набор команд процессора для вычисления абсолютного значения (в архитектуре Intel это FABS), что будет НАМНОГО быстрее, чем все, что вы делаете выше.
3. Ахмед Масуд: Вы уверены в этом? Я проверю это сейчас. … Хм, не совсем понимаю, почему, но оказалось, что простая функция fabs является самой быстрой: fabs — 0.922311, fastAbs (сверху) — 0.935108, Abs (также сверху) — 0.937011, abs на double — 0.936235. Мне немного любопытно…
Ответ №1:
Что касается первого вопроса, я не уверен, почему вы не можете просто использовать присваивание так, как хотите. Компилятор выполнит любую возможную оптимизацию.
В вашем втором примере кода. Вы нарушаете строгое сглаживание. Так что это не то же самое.
Что касается того, почему это медленнее:
Это потому, что процессоры сегодня, как правило, имеют отдельные целочисленные и плавающие единицы. Таким образом, вы принудительно перемещаете значение из одной единицы в другую. Это накладные расходы. (Это часто делается через память, поэтому у вас есть дополнительные загрузки и хранилища.)
Во втором фрагменте: a
который изначально находится в единице с плавающей запятой (либо в x87 FPU, либо в регистре SSE), необходимо переместить в регистры общего назначения, чтобы применить маску 0x7FFFFFFF
. Затем его нужно переместить обратно.
В первом фрагменте: компилятор, вероятно, достаточно умен, чтобы загружать a
непосредственно в целочисленную единицу. Таким образом, вы обходите FPU на первом этапе.
(Я не уверен на 100%, пока вы не покажете нам сборку. Это также будет сильно зависеть от того, начинается ли параметр в регистре или в стеке. И используется ли вывод немедленно другой операцией с плавающей запятой.)
Комментарии:
1. Первая версия также UB, вы можете читать только те члены объединения, которые были записаны в последний раз (если это не изменилось в C 11).
2. Я знаю, что в C99 вам разрешено делать это через объединения. Но я не уверен в C 11.
Ответ №2:
Глядя на дизассемблирование кода, скомпилированного в режиме выпуска, разница совершенно очевидна! Я удалил встроенную и использовал две виртуальные функции, чтобы позволить компилятору не слишком сильно оптимизировать и позволить нам показать различия.
Это первая функция.
013D1002 in al,dx
union {
float x;
int a;
} u;
u.x = x;
013D1003 fld dword ptr [x] // Loads a float on top of the FPU STACK.
013D1006 fstp dword ptr [x] // Pops a Float Number from the top of the FPU Stack into the destination address.
u.a amp;= 0x7FFFFFFF;
013D1009 and dword ptr [x],7FFFFFFFh // Execute a 32 bit binary and operation with the specified address.
return u.x;
013D1010 fld dword ptr [x] // Loads the result on top of the FPU stack.
}
Это вторая функция.
013D1020 push ebp // Standard function entry... i'm using a virtual function here to show the difference.
013D1021 mov ebp,esp
int b= *((int *)amp;a) amp; 0x7FFFFFFF;
013D1023 mov eax,dword ptr [a] // Load into eax our parameter.
013D1026 and eax,7FFFFFFFh // Execute 32 bit binary and between our register and our constant.
013D102B mov dword ptr [a],eax // Move the register value into our destination variable
return *((float *)(amp;b));
013D102E fld dword ptr [a] // Loads the result on top of the FPU stack.
Количество операций с плавающей запятой и использование стека FPU в первом случае больше.
Функции выполняют именно то, что вы просили, поэтому неудивительно.
Поэтому я ожидаю, что вторая функция будет быстрее.
Теперь … удаление виртуальных и встраивание немного отличаются, здесь сложно написать код дизассемблирования, потому что, конечно, компилятор хорошо справляется, но я повторяю, если значения не являются константами, компилятор будет использовать больше операций с плавающей запятой в первой функции. Конечно, целочисленные операции выполняются быстрее, чем операции с плавающей запятой.
Вы уверены, что прямое использование функции math.h abs работает медленнее, чем ваш метод? Если правильно встроено, функция abs просто сделает это!
00D71016 fabs
Подобные микрооптимизации трудно увидеть в длинном коде, но если ваша функция вызывается в длинной цепочке операций с плавающей запятой, fab будут работать лучше, поскольку значения уже будут в стеке FPU или в регистрах SSE! abs будет быстрее и лучше оптимизирован компилятором.
Вы не можете измерить производительность оптимизаций, выполняющих цикл в фрагменте кода, вы должны видеть, как компилятор смешивает все вместе в реальном коде.
Комментарии:
1. Я протестировал функцию now fabs, и она оказалась быстрее. Кажется, я был достаточно глуп, чтобы поверить в «факт», что ручная установка старшего значащего бита выполняется быстрее, чем fabs, но это оказалось неправдой. Теперь я удалю свою функцию ‘fastAbs’ и заменю ее на fabs. Спасибо за хорошее объяснение, ребята!
2. Я также протестировал его в своем приложении, и действительно, код выполняется немного быстрее! Рад, что я задал этот вопрос, потому что сортировка некоторых ненужных вещей нехороша.