Необычный быстрый обратный квадратный корень Джона Кармака (Quake III)

У Джона Кармака есть специальная функция в исходном коде 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;
}

person Alex    schedule 28.08.2009    source источник
comment
Вот объяснение   -  person sepp2k    schedule 29.08.2009
comment
Об этом писали миллионы раз. См .: google.com/search?q=0x5f3759df   -  person Greg Hewgill    schedule 29.08.2009
comment
Спасибо хоть. Это был гораздо более интересный вопрос, чем как сделать положительное число отрицательным в C #?   -  person MusiGenesis    schedule 29.08.2009
comment
Не был Кармак. en.wikipedia.org/wiki/Fast_inverse_square_root   -  person h4xxr    schedule 29.08.2009
comment
Чёрт возьми, это просто взлом, основанный на методе Ньютона, а не какой-то святой Грааль алгоритмов, перестаньте об этом говорить, мольбы: P   -  person ldog    schedule 03.09.2009
comment
Почему в этой строке i = * ( long * ) &y; адрес y берется как указатель на long, а затем снова разыменовывается?   -  person Nubcake    schedule 27.07.2017
comment
@Nubcake: потому что y - это float, и это переводит его в целое число. (Небезопасно, потому что это нарушает правила строгого псевдонима C. A union в C99 или memcpy в C89 / C ++ будет делать то же самое, следуя правилам языка, и компилирует то же самое, по крайней мере, с современными оптимизирующими компиляторами.)   -  person Peter Cordes    schedule 14.12.2017


Ответы (5)


К вашему сведению. Кармак этого не писал. Терье Матисен и Гэри Таролли частично (и весьма скромно) признают это, а также ссылаются на некоторые другие источники.

Как возникла мифическая константа, остается загадкой.

Процитирую Гэри Таролли:

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

Чуть лучшая константа, , разработанная опытным математиком (Крис Ломонт), пытаясь чтобы выяснить, как работал исходный алгоритм:

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;
}

Несмотря на это, его первоначальная попытка математически «превосходной» версии id sqrt (которая пришла к почти той же константе) оказалась хуже той, которая была первоначально разработана Гэри, несмотря на то, что математически была намного «более чистой». Он не мог объяснить, почему id был таким отличным iirc.

person Rushyo    schedule 28.08.2009
comment
Что должно означать «математически более чистое»? - person Tara; 30.07.2015
comment
Я мог бы представить, где первое предположение может быть получено из обоснованных констант, а не на кажущемся произвольном уровне. Хотя, если вам нужно техническое описание, вы можете его поискать. Я не математик, и семантическое обсуждение математической терминологии не относится к SO. - person Rushyo; 30.07.2015
comment
Это именно причина, по которой я заключил это слово в пугающие кавычки, чтобы избежать подобной чепухи. Полагаю, это предполагает, что читатель знаком с разговорной английской письменностью. Можно подумать, что здравого смысла будет достаточно. Я не использовал расплывчатый термин, потому что думал, что вы знаете что, я действительно хочу, чтобы меня спросили об этом кто-то, кто не потрудится найти исходный источник, что займет две секунды в Google. - person Rushyo; 31.07.2015
comment
Что ж, вы на самом деле не ответили на вопрос. - person BJovke; 13.02.2017
comment
Для тех, кто хотел знать, где он его находит: yond3d.com/content/articles/8 < / а> - person mr5; 27.06.2017
comment
Вы неправильно цитируете газету и создаете драму там, где ее не было. В статье Ломонта четко объясняется используемый алгоритм и способы его улучшения. - person johnwbyrd; 17.01.2020

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

Он просто показывает, как должна развиваться оптимизация по мере изменения характера лежащего в основе оборудования.

person Community    schedule 28.08.2009
comment
Тем не менее, это все еще намного быстрее, чем std :: sqrt (). - person Tara; 02.08.2015
comment
У вас есть источник? Я хочу протестировать среду выполнения, но у меня нет комплекта разработчика Xbox 360. - person DucRP; 16.12.2016
comment
Что ж, теперь в процессоре Intel есть rsqrt. Т.е. инструкция sse _mm_rsqrt_ss, и она еще быстрее там. - person aselle; 03.07.2021

Greg Hewgill и 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 будет идти, но останавливаться раньше, тем самым увеличивая скорость вычислений.

person BJovke    schedule 13.02.2017
comment
Приведение указателя к long является приближением log_2 (float). Откидывание назад занимает примерно 2 ^. Это означает, что вы можете сделать соотношение примерно линейным. - person wizzwizz4; 01.09.2017

Согласно этой красивой статье, написанной некоторое время назад ...

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

Это действительно хорошее чтение. Это лишь крошечный кусочек.

person Dillie-O    schedule 28.08.2009
comment
Упомянутый здесь метод Ньютона-Рафсона похож на градиентный спуск, используемый в нейронных сетях. Главная магия здесь - постоянство. Каким-то образом с помощью этой константы и одной итерации Ньютона Рафсона на ней было достаточно, чтобы достичь требуемой точности. - person Harsha Reddy; 18.10.2020

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

    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

person ThisIsAReallyOldQuestion    schedule 19.01.2018
comment
Это заслуживает большего внимания. Все это имеет смысл после того, как вы поймете, что это всего лишь квадратный корень из 2 ^ 127 ... - person u8y7541; 05.04.2018
comment
Просто для полноты - шестнадцатеричный код - это не совсем sqrt(2^127), а близкое приближение (до двух цифр MSB). sqrt(2^127) = 1.3043x10^19 в то время как 0x5F3759DF = 1.3211x10^19 - person Loves Probability; 05.01.2021