Почему 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() и позволить "умному" компилятору "оптимизировать". При 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, но структура данных массива здесь предназначена только для тестирования: то, что я действительно пытаюсь измерить, - это скалярная производительность для использования в коде, который не может быть векторизован.)

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

Ньютон-Рафсон сходится к нулю 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, что тоже легко.

Ответ 5

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