Почему скаляр SSE sqrt (x) медленнее, чем rsqrt (x) * x? - PullRequest
102 голосов
/ 07 октября 2009

Я профилировал некоторые наши основные математические вычисления на 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 нс / с плавающей запятой с использованием FPU x87 это было патетически плохо:

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

Следующее, что я попробовал, было использование встроенной функции, чтобы заставить компилятор использовать скалярный код операции SSE в формате 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 / & radic; x = & radic; x). Несмотря на то, что для этого требуются две зависимые операции, на сегодняшний день это было самое быстрое решение: 1,24 нс / с плавающей запятой и с точностью до 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; но структура данных массива здесь только для целей тестирования: что Я действительно пытаюсь измерить скалярную производительность для использования в коде, который нельзя векторизовать.)

Ответы [ 5 ]

207 голосов
/ 07 октября 2009

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

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

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

7 голосов
/ 12 июля 2011

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

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

5 голосов
/ 07 октября 2009

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

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

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

3 голосов
/ 03 августа 2012

Ньютон-Рафсон сходится к нулю 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)

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

И: шаг полного обновления new_x = x + dx, затем читается:

x *= 3/2 - y/2 * x * x что тоже легко.

0 голосов
/ 05 июля 2016

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

...