Я профилировал часть нашей основной математики на 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, используя x90 FPU, это было патетически плохо:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
Следующее, что я пробовал, это использовать встроенный способ заставить компилятор использовать SSE-скалярный код sqrt:
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.9ns/float. Я также попробовал метод аппроксимации Ньютона-Рапсона в кармаке, который работал даже лучше, чем аппаратное обеспечение, при 4.3ns/float, хотя с ошибкой 1 в 2 10 (что слишком для моих целей).
Дозировка была, когда я попробовал SSE op для обратного квадратного корня, а затем использовал умножить, чтобы получить квадратный корень (x * 1/& radic; x = & radic; x). Несмотря на то, что это требует двух зависимых операций, это было самое быстрое решение на уровне 1.24ns/float и с точностью до 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
&mdash, но структура данных массива здесь предназначена только для тестирования: то, что я действительно пытаюсь измерить, - это скалярная производительность для использования в коде, который не может быть векторизован.)