Я профилировал некоторые из наших основных математических вычислений на Intel Core Duo, и, глядя на различные подходы к вычислению квадратного корня, я заметил кое-что странное: используя скалярные операции SSE, быстрее получить обратный квадратный корень и умножить его. чтобы получить sqrt, чем использовать собственный код операции sqrt!
Я тестирую это с помощью цикла, например:
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
Я пробовал это с несколькими разными телами для TestSqrtFunction, и у меня есть некоторые тайминги, которые действительно ломают мне голову. Хуже всего было использование встроенной функции sqrt () и возможность «оптимизировать» «умный» компилятор. При 24ns / float с использованием FPU x87 это было ужасно плохо:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
Следующее, что я пробовал, - это использовать встроенную функцию, чтобы заставить компилятор использовать скалярный код операции sqrt SSE:
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
Это было лучше, 11,9 нс / плавучесть. Я также попробовал дурацкую технику аппроксимации Ньютона-Рафсона Кармака , которая работала даже лучше, чем оборудование, при 4,3 нс / число с плавающей запятой, хотя и с ошибкой 1 из 2 10 (что слишком много для моих целей).
Неприятность была, когда я попробовал операцию SSE для получения обратного квадратного корня, а затем использовал умножение, чтобы получить квадратный корень (x * 1 / √x = √x). Даже если это занимает две зависимые операции, он был самым быстрым решением на сегодняшний день, в 1.24ns / поплавком и с точностью до 2 -14 :
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
Мой вопрос в основном в том, что дает ? Почему встроенный аппаратный код операции извлечения квадратного корня в SSE медленнее, чем синтез его из двух других математических операций?
Я уверен, что это действительно стоимость самой операции, потому что я подтвердил:
- Все данные помещаются в кеш, и доступ осуществляется последовательно
- функции встроены
- разворачивание цикла не имеет значения
- флаги компилятора выставлены на полную оптимизацию (и сборка хорошая, я проверил)
( edit : stephentyrone правильно указывает, что операции с длинными строками чисел должны использовать векторизованные SIMD-упакованные операции, например, rsqrtps
- но структура данных массива здесь предназначена только для целей тестирования: то, что я действительно пытаюсь измерить, - это скалярная производительность для использования в коде которые нельзя векторизовать.)
inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
. Но это плохая идея, потому что это может легко вызвать остановку загрузки-попадания-сохранения, если ЦП записывает числа с плавающей запятой в стек, а затем сразу же считывает их обратно - в частности, перестановка из векторного регистра в регистр с плавающей точкой для возвращаемого значения плохие новости. Кроме того, коды операций базовой машины, которые встроенные функции SSE представляют, в любом случае принимают адресные операнды.
eax
) очень плохое, в то время как круговой обход между xmm0 и стеком и обратно нет из-за переадресации магазина Intel. Вы можете рассчитать время сами, чтобы убедиться в этом. Как правило, самый простой способ увидеть потенциальную LHS - это посмотреть на выпущенную сборку и увидеть, где данные перебираются между наборами регистров; ваш компилятор может сделать умную вещь, а может и нет. Что касается нормализации векторов, я записал свои результаты здесь: bit.ly/9W5zoU