Я не нашел четкого ориентира по этому вопросу, поэтому сделал один. Я опубликую это здесь в случае, если кто-то ищет это как я.
У меня есть один вопрос, хотя. Разве SSE не должен быть в 4 раза быстрее, чем RSQRT с четырьмя fpu в цикле? Это быстрее, но всего в 1,5 раза. Оказывает ли такое влияние переход на регистры SSE, потому что я не делаю много вычислений, а только rsqrt? Или потому, что SSE rsqrt намного точнее, как мне узнать, сколько итераций выполняет sse rsqrt? Два результата:
4 align16 float[4] RSQRT: 87011us 2236.07 - 2236.07 - 2236.07 - 2236.07
4 SSE align16 float[4] RSQRT: 60008us 2236.07 - 2236.07 - 2236.07 - 2236.07
редактировать
Скомпилировано с использованием MSVC 11 /GS- /Gy /fp:fast /arch:SSE2 /Ox /Oy- /GL /Oi
на AMD Athlon II X2 270
Тестовый код:
#include <iostream>
#include <chrono>
#include <th/thutility.h>
int main(void)
{
float i;
//long i;
float res;
__declspec(align(16)) float var[4] = {0};
auto t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
res = sqrt(i);
auto t2 = std::chrono::high_resolution_clock::now();
std::cout << "1 float SQRT: " << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us " << res << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
thutility::math::rsqrt(i, res);
res *= i;
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "1 float RSQRT: " << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us " << res << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
thutility::math::rsqrt(i, var[0]);
var[0] *= i;
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "1 align16 float[4] RSQRT: " << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us " << var[0] << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
thutility::math::rsqrt(i, var[0]);
var[0] *= i;
thutility::math::rsqrt(i, var[1]);
var[1] *= i + 1;
thutility::math::rsqrt(i, var[2]);
var[2] *= i + 2;
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "3 align16 float[4] RSQRT: "<< std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us "<< var[0] << " - " << var[1] << " - " << var[2] << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
thutility::math::rsqrt(i, var[0]);
var[0] *= i;
thutility::math::rsqrt(i, var[1]);
var[1] *= i + 1;
thutility::math::rsqrt(i, var[2]);
var[2] *= i + 2;
thutility::math::rsqrt(i, var[3]);
var[3] *= i + 3;
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "4 align16 float[4] RSQRT: "<< std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us "<< var[0] << " - " << var[1] << " - " << var[2] << " - " << var[3] << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
var[0] = i;
__m128& cache = reinterpret_cast<__m128&>(var);
__m128 mmsqrt = _mm_rsqrt_ss(cache);
cache = _mm_mul_ss(cache, mmsqrt);
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "1 SSE align16 float[4] RSQRT: " << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count()
<< "us " << var[0] << std::endl;
t1 = std::chrono::high_resolution_clock::now();
for(i = 0; i < 5000000; i+=1)
{
var[0] = i;
var[1] = i + 1;
var[2] = i + 2;
var[3] = i + 3;
__m128& cache = reinterpret_cast<__m128&>(var);
__m128 mmsqrt = _mm_rsqrt_ps(cache);
cache = _mm_mul_ps(cache, mmsqrt);
}
t2 = std::chrono::high_resolution_clock::now();
std::cout << "4 SSE align16 float[4] RSQRT: "<< std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "us " << var[0] << " - "<< var[1] << " - " << var[2] << " - " << var[3] << std::endl;
system("PAUSE");
}
Использование результатов поплавок тип:
1 float SQRT: 24996us 2236.07
1 float RSQRT: 28003us 2236.07
1 align16 float[4] RSQRT: 32004us 2236.07
3 align16 float[4] RSQRT: 51013us 2236.07 - 2236.07 - 5e+006
4 align16 float[4] RSQRT: 87011us 2236.07 - 2236.07 - 2236.07 - 2236.07
1 SSE align16 float[4] RSQRT: 46999us 2236.07
4 SSE align16 float[4] RSQRT: 60008us 2236.07 - 2236.07 - 2236.07 - 2236.07
Мой вывод не стоит возиться с SSE2, если мы не сделаем расчеты не менее чем по 4 переменным. (Может быть, это относится только к rsqrt, но это дорогостоящий расчет (он также включает в себя несколько умножений), поэтому, вероятно, он применим и к другим вычислениям)
Кроме того, sqrt (x) быстрее, чем x * rsqrt (x) с двумя итерациями, а x * rsqrt (x) с одной итерацией слишком неточен для вычисления расстояния.
Так что утверждения, которые я видел на некоторых досках, что x * rsqrt (x) быстрее, чем sqrt (x), неверны. Так что это не логично и не стоит потери точности использовать rsqrt вместо sqrt, если только вам не нужно 1 / x ^ (1/2).
Пробовал без флага SSE2 (если он применял SSE в обычном цикле rsqrt, он давал те же результаты).
Мой RSQRT является модифицированной (той же) версией Quake rsqrt.
namespace thutility
{
namespace math
{
void rsqrt(const float& number, float& res)
{
const float threehalfs = 1.5F;
const float x2 = number * 0.5F;
res = number;
uint32_t& i = *reinterpret_cast<uint32_t *>(&res); // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
res = res * ( threehalfs - ( x2 * res * res ) ); // 1st iteration
res = res * ( threehalfs - ( x2 * res * res ) ); // 2nd iteration, this can be removed
}
}
}
Легко получить много ненужных накладных расходов в коде SSE.
Если вы хотите убедиться, что ваш код эффективен, посмотрите на разборку компилятора. Одна вещь, которая часто убивает производительность (и, похоже, может повлиять на вас), — это перемещение данных между памятью и регистрами SSE без необходимости.
Внутри вашего цикла вы должны хранить все соответствующие данные, а также результат, в регистрах SSE, а не в float[4]
,
Пока вы обращаетесь к памяти, убедитесь, что компилятор генерирует выровненную инструкцию перемещения, чтобы загрузить данные в регистры или записать их обратно в массив.
И убедитесь, что в сгенерированных инструкциях SSE нет большого количества ненужных инструкций по перемещению и другой пропасти между ними. Некоторые компиляторы довольно ужасно генерируют код SSE из встроенных функций, поэтому стоит следить за генерируемым кодом.
Наконец, вам нужно обратиться к руководству / спецификациям вашего процессора, чтобы убедиться, что он действительно выполняет упакованные инструкции, которые вы используете, так же быстро, как скалярные инструкции. (Что касается современных процессоров, я бы поверил, что они это делают, но некоторые старые процессоры, по крайней мере, требуют немного дополнительного времени для упакованных инструкций. Не в четыре раза длиннее скалярного, но достаточно, чтобы вы не смогли достичь ускорения в 4 раза )
Мой вывод не стоит возиться с SSE2, если мы не сделаем расчеты не менее чем по 4 переменным. (Может быть, это относится только к rsqrt, но это дорогостоящий расчет (он также включает в себя несколько умножений), поэтому, вероятно, он применим и к другим вычислениям)
Кроме того, sqrt (x) быстрее, чем x * rsqrt (x) с двумя итерациями, а x * rsqrt (x) с одной итерацией слишком неточен для вычисления расстояния.
Так что утверждения, которые я видел на некоторых досках, что x * rsqrt (x) быстрее, чем sqrt (x), неверны. Так что это не логично и не стоит потери точности использовать rsqrt вместо sqrt, если только вам не нужно 1 / x ^ (1/2).