Незвичайний швидкий зворотний квадратний корінь Джона Кармака (Quake III)


112

Джон Кармак має спеціальну функцію у вихідному коді Quake III, яка обчислює зворотний квадратний корінь поплавця, в 4 рази швидше, ніж звичайний (float)(1.0/sqrt(x)), включаючи дивну 0x5f3759dfконстанту. Дивіться код нижче. Чи може хтось пояснити по черзі, що саме тут відбувається і чому це працює так швидше, ніж звичайна реалізація?

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;
  i  = 0x5f3759df - ( i >> 1 );
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) );

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) );
  #endif
  #endif
  return y;
}


10
Про це писали мільйони разів. Дивіться: google.com/search?q=0x5f3759df
Greg Hewgill

15
Спасибі, хоча. Це було набагато цікавіше питання, ніж "як зробити позитивне число від'ємним у C #?"
MusiGenesis


7
Святе дерьмо, це просто хак, заснований на методі Ньютона, його не якийсь святий мотив алгоритмів, перестань говорити про це благання: P
ldog

Відповіді:


75

FYI. Кармак не написав цього. Тер'є Матісен і Гері Тароллі беруть частковий (і дуже скромний) кредит для цього, а також кредитують деякі інші джерела.

Як отримана міфічна константа - це таємниця.

Щоб цитувати Гарі Тароллі:

Що насправді робить обчислення з плаваючою комою в цілому, - це знадобилося багато часу, щоб зрозуміти, як і чому це працює, і я вже не пам'ятаю деталей.

Трохи краща константа, розроблена експертом-математиком (Кріс Ломон), намагаючись розібратися, як працював оригінальний алгоритм:

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int*)&x;              // get bits for floating value
    i = 0x5f375a86 - (i >> 1);      // gives initial guess y0
    x = *(float*)&i;                // convert bits back to float
    x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    return x;
}

Незважаючи на це, його первісна спроба математично "перевершеної" версії sqrt id (яка дійшла майже до тієї самої константи) виявилася неповноцінною в порівнянні з початково розробленою Гарі, незважаючи на те, що він був математично набагато "чистішим". Він не міг пояснити, чому ідентичні виявилися такими чудовими думками.


4
Що має означати "математично чистіше"?
Тара

1
Я б міг уявити, де перша здогадка може бути отримана з виправданих констант, а не з зовнішнього вигляду довільної. Хоча, якщо ви хочете технічний опис, ви можете його переглянути. Я не математик, і семантична дискусія про математичну термінологію не належить до ПП.
Rushyo

7
Ось саме тому я інкапсульований це слово в лапках, дефіцитних , щоб запобігти такого роду нонсенс. Це передбачає, що читач знайомий з розмовною англійською писемністю, я думаю. Ви думаєте, здорового глузду буде достатньо. Я не використовував розпливчастий термін, тому що думав, що "ви знаєте що, я дійсно хочу, щоб мене про це запитали хтось, хто не може потурбуватися шукати оригінальне джерело, яке би зайняло дві секунди в Google".
Rushyo

2
Ну, ви насправді не відповіли на питання.
BJovke

1
Для тих, хто хотів дізнатися, де він його знаходить : yond3d.com/content/articles/8
mr5

52

Звичайно, у наші дні це виявляється набагато повільніше, ніж просто використання sqrt FPU (особливо на 360 / PS3), тому що заміна між регістрами float та int викликає сховище навантаження, тоді як блок з плаваючою комою може робити зворотний квадрат корінь в апаратному забезпеченні.

Це просто показує, як оптимізація повинна розвиватися в міру природи основних апаратних змін.


4
Це все ще набагато швидше, ніж std :: sqrt (), хоча.
Тара

2
У вас є джерело? Я хочу перевірити час виконання, але у мене немає набору для розробки Xbox 360.
DucRP

31

Грег Х'югілл і IllidanS4 дали посилання з відмінним математичним поясненням. Я спробую підсумувати це для тих, хто не хоче надто детально вникати.

Будь-яка математична функція, за деякими винятками, може бути представлена ​​багаточленною сумою:

y = f(x)

можна точно перетворити на:

y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...

Де a0, a1, a2, ... - константи . Проблема полягає в тому, що для багатьох функцій, таких як квадратний корінь, для точного значення ця сума має нескінченну кількість членів, вона не закінчується на деякому x ^ n . Але, якщо ми зупинимось на деякому x ^ n, ми все одно матимемо результат до деякої точності.

Отже, якщо ми маємо:

y = 1/sqrt(x)

У цьому конкретному випадку вони вирішили відкинути всі члени полінома вище секунди, можливо, через швидкість обчислення:

y = a0 + a1*x + [...discarded...]

І тепер завдання зійшло обчислити a0 і a1, щоб y мав найменшу різницю від точного значення. Вони підрахували, що найбільш підходящі значення:

a0 = 0x5f375a86
a1 = -0.5

Отже, коли ви покладете це в рівняння, ви отримаєте:

y = 0x5f375a86 - 0.5*x

Що таке саме рядок, який ви бачите в коді:

i = 0x5f375a86 - (i >> 1);

Редагувати: насправді тут y = 0x5f375a86 - 0.5*xне те саме, що, i = 0x5f375a86 - (i >> 1);оскільки зсув плавця як ціле число не тільки ділиться на два, але й ділить експонент на два і викликає деякі інші артефакти, але все ж зводиться до обчислення деяких коефіцієнтів a0, a1, a2 ....

У цей момент вони з'ясували, що точності цього результату недостатньо для досягнення мети. Таким чином, вони також зробили лише один крок ітерації Ньютона для підвищення точності результату:

x = x * (1.5f - xhalf * x * x)

Вони могли зробити ще кілька ітерацій у циклі, кожен з яких покращує результат, поки не буде досягнуто необхідної точності. Саме так воно працює в CPU / FPU! Але, здається, було достатньо лише однієї ітерації, що також було благом для швидкості. CPU / FPU робить стільки ітерацій, скільки потрібно для досягнення точності для числа з плаваючою комою, в якому зберігається результат, і він має більш загальний алгоритм, який працює для всіх випадків.


Отже, коротше, що вони зробили:

Використовуйте (майже) той же алгоритм, що і CPU / FPU, використовуйте поліпшення початкових умов для особливого випадку 1 / sqrt (x) і не обчислюйте весь шлях до точності CPU / FPU піде, але зупиниться раніше, таким чином набираючи швидкість обчислення.


2
Закидання покажчика на довгий - це наближення log_2 (float). Відкидання його назад - це наближення в довжину 2 ^. Це означає, що ви можете зробити відношення приблизно лінійним.
wizzwizz4

22

Згідно з цією приємною статтею, написаною за час назад ...

Магія коду, навіть якщо ви не можете його дотримуватися, виділяється як i = 0x5f3759df - (i >> 1); рядок. Спрощено, Ньютон-Рафсон - це наближення, яке починається з здогадки і уточнює його за допомогою ітерації. Користуючись природою 32-розрядних процесорів x86, я, ціле число, спочатку встановлюється значенням числа з плаваючою точкою, для якого потрібно прийняти зворотний квадрат, використовуючи ціле число. Потім я встановлюється на 0x5f3759df, мінус сам змістив один біт праворуч. Правий зсув скидає найменш значущий біт i, по суті, вдвічі зменшуючи його.

Це справді добре прочитане. Це лише крихітний шматок.


19

Мені було цікаво побачити, яка константа є плаваючою, тому я просто написав цей біт коду і гугл цілим числом, яке вискочило.

    long i = 0x5F3759DF;
    float* fp = (float*)&i;
    printf("(2^127)^(1/2) = %f\n", *fp);
    //Output
    //(2^127)^(1/2) = 13211836172961054720.000000

Схоже, що константа - це "ціле число наближення до квадратного кореня 2 ^ 127, більш відомого за шістнадцятковою формою його представлення з плаваючою комою, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

На цьому ж сайті це пояснює все. https://mrob.com/pub/math/numbers-16.html#le009_16


6
Це заслуговує на більшу увагу. Все має сенс, зрозумівши, що це просто квадратний корінь 2 ^ 127 ...
u8y7541
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.