Почему SSE скалярный sqrt(x) медленнее, чем rsqrt (x) * x?


Я профилировал некоторые из наших основных математических операций на 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() и позволить "умному" компилятору "оптимизировать". В 24нс/поплавок, с помощью операций с плавающей запятой x87, так это было трогательно плохо:

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,9 НС / поплавок. Я тоже пробовал дурацкий метод приближения Ньютона-Рафсона Кармака, который побежал даже лучше, чем аппаратное обеспечение, на 4,3 НС / поплавок, хотя и с ошибкой 1 в 210 (что слишком много для моих целей).

doozy был, когда я попробовал SSE op для взаимные квадратный корень, а затем используется умножение, чтобы получить квадратный корень ( x * 1/√x = √x ). Несмотря на то, что это требует двух зависимых операций, это было самое быстрое решение на сегодняшний день, на 1.24 НС/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 правильно указывает, что операции на длинные строки чисел следует использовать векторизацию Симд упакованные ОПС, как rsqrtps - но структура данных массива здесь предназначена только для тестирования: то, что я действительно пытаюсь измерить, это скаляр производительность для использования в коде, который не может быть векторизован.)

5 99

5 ответов:

sqrtss дает правильно округленный результат. rsqrtss дает приближение к взаимному, точному до около 11 битов.

sqrtss создает гораздо более точный результат, когда требуется точность. rsqrtss существует для случаев, когда достаточно приближения, но требуется скорость. Если Вы читаете документацию Intel, вы также найдете последовательность команд (взаимное приближение квадратного корня, за которым следует один шаг Ньютона-Рафсона), который дает почти полную точность (~23 бит точности, если я правильно помню), и все еще несколько быстрее, чем sqrtss.

edit: если скорость критична, и вы действительно вызываете это в цикле для многих значений, вы должны использовать векторизованные версии этих инструкций,rsqrtps или sqrtps, оба из которых обрабатывают 4 поплавка в инструкцию.

Это также верно для деления. MULSS(а RCPSS(Б)) является способ быстрее, чем DIVSS(а,б). На самом деле это все еще быстрее, даже если вы увеличиваете его точность с помощью итерации Ньютона-Рафсона.

Intel и AMD рекомендуют этот метод в своих руководствах по оптимизации. В приложениях, которые не требуют соответствия IEEE-754, единственной причиной использования div/sqrt является читаемость кода.

вместо того, чтобы дать ответ, который на самом деле может быть неверным (я также не собираюсь проверять или спорить о Кеше и других вещах, скажем, они идентичны), я попытаюсь указать вам источник, который может ответить на ваш вопрос.
Разница может заключаться в том, как вычисляются sqrt и rsqrt. Вы можете прочитать больше здесь http://www.intel.com/products/processor/manuals/. я бы предложил начать с чтения о функциях процессора, которые вы используете, есть некоторая информация, особенно о rsqrt (cpu использует внутреннюю таблицу поиска с огромным приближением, что значительно упрощает получение результата). Может показаться, что rsqrt настолько быстрее, чем sqrt, что 1 дополнительная операция mul (которая не является дорогостоящей) не может изменить ситуацию здесь.

Edit: несколько фактов, которые могут стоить упоминания:
1. Когда-то я делал некоторые микрооптимизации для своей графической библиотеки, и я использовал rsqrt для вычисления длины векторов. (вместо корня, Я умножил свою сумму в квадрате на rsqrt из нее, что именно то, что вы сделали в своих тестах), и он работал лучше.
2. Вычисление rsqrt с использованием простой таблицы поиска может быть проще, так как для rsqrt, когда x переходит в бесконечность, 1/sqrt(x) переходит в 0, поэтому для небольших x значения функции не меняются (много), тогда как для sqrt - это бесконечность, так что это простой случай ;).

кроме того, уточнение: я не уверен, где я нашел его в книгах, которые я связал, но я довольно конечно, я читал, что rsqrt использует некоторую таблицу поиска, и ее следует использовать только тогда, когда результат не должен быть точным, хотя - я тоже могу ошибаться, как это было некоторое время назад :).

Ньютон-Рафсон сходится к нулю f(x) использование приращений равно -f/f' здесь f' производная.

на x=sqrt(y), вы можете попробовать решить f(x) = 0 на x используя f(x) = x^2 - y;

тогда приращение:dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x который имеет медленное разделение в нем.

вы можете попробовать другие функции (например,f(x) = 1/y - 1/x^2), но они будут одинаково сложными.

давайте посмотрим на 1/sqrt(y) сейчас. Вы можете попробовать f(x) = x^2 - 1/y, но это будет одинаково сложно: dx = 2xy / (y*x^2 - 1) например. Один неочевидный альтернативный выбор для f(x) - Это: f(x) = y - 1/x^2

затем: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

Ах! Это не тривиальное выражение, но в нем есть только умножение, без деления. = > Быстрее!

и: полный шаг обновления гласит:

x *= 3/2 - y/2 * x * x это слишком просто.

это быстрее, потому что эти инструкции игнорируют режимы округления и не обрабатывают исключения с плавающей точкой или dernormalized числа. По этим причинам гораздо проще конвейеризировать, спекулировать и выполнять другие инструкции fp не по порядку.