Чому SSE скалярний sqrt (x) повільніше, ніж rsqrt (x) * x?


106

Я профілював частину нашої основної математики на 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, використовуючи x87 FPU, це було патетично погано:

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

Наступне, що я спробував, - це використовувати внутрішнє слово, щоб змусити компілятор використовувати скалярний код кодування SST 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,9ns / float. Я також спробував хитрую техніку наближення Ньютона-Рафсона від Carmack, яка працювала навіть краще, ніж апаратне забезпечення, зі швидкістю 4,3ns / float, хоча з помилкою 1 на 2 10 (що занадто багато для моїх цілей).

Дузі було, коли я спробував SSE op для зворотного квадратного кореня, а потім використав множення, щоб отримати квадратний корінь (x * 1 / √x = √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 квадратний кореневий кодовий опкод проходить повільніше, ніж синтезувати його з двох інших математичних операцій?

Я впевнений, що це дійсно вартість самої опції, тому що я перевірив:

  • Усі дані вміщуються в кеш, а доступ є послідовним
  • функції вбудовані
  • розгортання циклу не має значення
  • прапори компілятора встановлені на повну оптимізацію (і збірка хороша, я перевірив)

( відредагувати : stephentyrone правильно вказує, що для операцій над довгими рядками чисел слід використовувати векторизуючу SIMP-пакет, наприклад, rsqrtps- але структура даних масиву тут призначена лише для тестування: те, що я насправді намагаюся вимірювати, - це скалярна ефективність для використання в коді це неможливо векторизувати.)


13
x / sqrt (x) = sqrt (x). Або, по-іншому: x ^ 1 * x ^ (- 1/2) = x ^ (1 - 1/2) = x ^ (1/2) = sqrt (x)
Crashworks

6
звичайно, inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }. Але це погана ідея, оскільки вона може легко викликати стійло-завантаження магазину, якщо ЦП записує поплавці в стек, а потім негайно зчитує їх назад - жонглюючи з векторного регістра до плаваючого регістра для повернення значення, зокрема погана новина. Крім того, базові машини кодують, що представляють внутрішньосистеми SSE, все одно приймають операнди адреси.
Crashworks

4
Скільки має значення LHS, залежить від конкретного роду та крокової дії даного x86: мій досвід полягає в тому, що на будь-якому рівні до i7 переміщення даних між наборами регістрів (наприклад, FPU до SSE до eax) дуже погано, в той час як обхід між xmm0 та стеком а назад - не через переадресацію магазину Intel. Ви можете встигнути самостійно, щоб точно побачити. Взагалі найпростіший спосіб побачити потенційний LHS - це переглянути випромінювану збірку і побачити, де дані переміщуються між наборами регістрів; ваш компілятор може зробити розумну справу, а може і не зробити. Що стосується нормалізації векторів, я написав свої результати тут: bit.ly/9W5zoU
Crashworks

2
Для PowerPC так: IBM має симулятор процесора, який може передбачати LHS та багато інших бульбашок конвеєра за допомогою статичного аналізу. Деякі КПП також мають апаратний лічильник LHS, який ви можете опитувати. Це складніше для x86; хороші інструменти профілювання дефіцитніші (VTune дещо зламаний в наші дні), а упорядковані трубопроводи менш детерміновані. Ви можете спробувати її виміряти емпіричним шляхом, вимірюючи інструкції за цикл, що можна зробити точно за допомогою апаратних лічильників продуктивності. У «інструкції відставний» і «повні цикли» регістри можуть бути лічені з , наприклад , PAPI або PerfSuite ( bit.ly/an6cMt ).
Crashworks

2
Ви також можете просто написати декілька перестановок на функцію та присвоїти їм час, щоб побачити, чи страждають вони особливо від стійлів. Intel не публікує багато деталей про те, як працюють їхні трубопроводи (про те, що вони LHS взагалі є якоюсь брудною таємницею), тому багато чого я дізнався, переглядаючи сценарій, який спричиняє затримку в інших арках (наприклад, PPC ), а потім побудувати контрольований експеримент, щоб перевірити, чи є у нього також x86.
Crashworks

Відповіді:


216

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

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

редагувати: Якщо швидкість є критичною, і ви дійсно викликаєте це в циклі для багатьох значень, вам слід використовувати векторизовані версії цих інструкцій, rsqrtpsабо sqrtpsобидві, які обробляють чотири поплавця на інструкцію.


3
Крок n / r дає точність 22 біт (це подвоює її); 23-біт був би точно повної точності.
Джаспер Беккерс

7
@Jasper Bekkers: Ні, не буде. По-перше, float має 24 біти точності. По- друге, sqrtssце правильно закруглені , який вимагає ~ 50 біт до округлення, і не може бути досягнуто з допомогою простої N / R ітерації в одинарної точності.
Стівен Канон

1
Це, безумовно, причина. Для розширення цього результату: проект Embree Intel ( software.intel.com/en-us/articles/… ) використовує для своєї математики векторизацію. Ви можете завантажити джерело за цим посиланням і подивитися, як вони роблять свої 3/4 D вектори. Для їх векторної нормалізації використовується rsqrt з наступною ітерацією ньютона-рафсона, яка потім дуже точна і все ж швидша, ніж 1 / ssqrt!
Брендон Пелфрі

7
Невеликий застереження: x rsqrt (x) призводить до NaN, якщо x дорівнює нулю або нескінченності. 0 * rsqrt (0) = 0 * INF = NaN. INF rsqrt (INF) = INF * 0 = NaN. З цієї причини CUDA на графічних процесорах NVIDIA обчислює приблизні одноточні квадратні корені у вигляді рециркуляції (rsqrt (x)), при цьому апаратне забезпечення забезпечує швидке наближення до зворотного та зворотного квадратного кореня. Очевидно, також можливі явні перевірки, що стосуються двох спеціальних випадків (але це буде повільніше для GPU).
njuffa

@BrandonPelfrey У якому файлі ви знайшли крок Ньютона Рапсона?
fredoverflow

7

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

Intel та AMD рекомендують цю методику в своїх посібниках з оптимізації. У додатках, які не потребують відповідності IEEE-754, єдиною причиною використання div / sqrt є читабельність коду.


1
Бродвелл та пізніше мають кращу продуктивність FP для поділу, тому компілятори на зразок кланг вибирають не використовувати зворотні + Ньютон для скалярних на останніх процесорах, оскільки це зазвичай не швидше. У більшості циклів divце не єдина операція, тому загальна пропускна здатність часто є вузьким місцем, навіть коли є divpsабо divss. Див. Розділення з плаваючою комою на множення з плаваючою комою , де в моїй відповіді є розділ про те, чому rcppsбільше не виграш. (Або виграш затримки) та числа на пропускну здатність / затримку.
Пітер Кордес

Якщо ваші вимоги до точності настільки низькі, що ви можете пропустити ітерацію Ньютона, так, так a * rcpss(b)можна швидше, але це все-таки більше, ніж a/b!
Пітер Кордес

5

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

Редагувати: Мало фактів, які, можливо, варто згадати:
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 що теж легко.


2

Є ще ряд відповідей на це вже з декількох років тому. Ось, що консенсус отримав право:

  • Інструкції rsqrt * обчислюють наближення до прямого прямокутного корінця, добре приблизно до 11-12 біт.
  • Він реалізований за допомогою таблиці пошуку (тобто ПЗУ), індексованої мантісою. (Насправді, це стисла таблиця пошуку, подібна до старих математичних таблиць, використовуючи коригування бітів низького порядку для економії на транзисторах.)
  • Причина, чому вона доступна, полягає в тому, що це початкова оцінка, яку використовує ФПУ для "справжнього" алгоритму квадратного кореня.
  • Є також приблизна відповідна інструкція, rcp. Обидві ці інструкції є підказкою щодо того, як ФПУ реалізує квадратний корінь та поділ.

Ось що консенсус помилився:

  • ФПУ епохи SSE не використовують Ньютона-Рафсона для обчислення квадратних коренів. Це чудовий метод у програмному забезпеченні, але було б помилкою реалізувати його таким чином у апаратному забезпеченні.

Алгоритм NR для обчислення зворотного квадратного кореня має цей крок оновлення, як зазначали інші:

x' = 0.5 * x * (3 - n*x*x);

Це багато множинних даних, що залежать від даних, і одне віднімання.

Далі йде алгоритм, який фактично використовують сучасні ФПУ.

Враховуючи b[0] = n, припустимо, ми можемо знайти ряд чисел Y[i]таких, що b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2підходять до 1. Тоді розглянемо:

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

Чітко x[n]підходи sqrt(n)та y[n]підходи 1/sqrt(n).

Ми можемо використовувати крок оновлення Ньютона-Рафсона для зворотного квадратного кореня, щоб отримати хороший Y[i]:

b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

Тоді:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

і:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

Наступне ключове спостереження - це b[i] = x[i-1] * y[i-1]. Так:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

Тоді:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

Тобто, з огляду на початкові x і y, ми можемо використовувати наступний крок оновлення:

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

Або, навіть фантазії, ми можемо встановити h = 0.5 * y. Це ініціалізація:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

І це крок оновлення:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

Це алгоритм Гольдшмідта, і він має величезну перевагу, якщо ви реалізуєте його в апаратному забезпеченні: "внутрішній цикл" - це три множинні додавання і більше нічого, а два з них є незалежними і можуть бути конвеєрними.

У 1999 р. FPU вже потребували конвеєрної схеми додавання / підкладки та конвеєра множення конвеєра, інакше SSE не буде дуже "потоковим". У 1999 році була потрібна лише одна з кожної схеми, щоб повністю реалізувати цей внутрішній цикл, не витрачаючи багато обладнання на квадратний корінь.

Сьогодні, звичайно, ми злили багаторазове додавання, виставлене на програміста. Знову ж таки, внутрішня петля - це три конвеєрні FMA, які (знову ж таки) загалом корисні, навіть якщо ви не обчислюєте квадратні корені.


1
Пов'язане: Як sqrt () GCC працює після компіляції? Який метод кореня використовується? Ньютон-Рафсон? має деякі посилання на конструкції апаратних пристроїв div / sqrt. Швидкий векторизований rsqrt і зворотний з SSE / AVX залежно від точності - одна ітерація Ньютона в програмному забезпеченні, з FMA або без нього, для використання з _mm256_rsqrt_psперфліновим аналізом Haswell. Зазвичай лише гарна ідея, якщо у вас немає іншої роботи в циклі, і ви б дуже важко зайнялися пропускною здатністю дільника. HW sqrt є єдиним, тому добре змішується з іншими роботами.
Пітер Кордес

-2

Це швидше, оскільки ці інструкції ігнорують режими округлення, і не обробляють винятки з плаваючою точкою або ненормалізовані числа. З цих причин набагато простіше планувати, спекулювати та виконувати іншу інструкцію з фп.


Очевидно неправильно. FMA залежить від поточного режиму округлення, але він має пропускну здатність дві на годину на Haswell та пізніші. З двома повністю конвеєрними підрозділами FMA, Haswell може мати до 10 FMA в польоті одночасно. Правильна відповідь rsqrt«s набагато нижче точність, що означає набагато менше роботи (або взагалі?) Після табличній отримати початкову припущення.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.