Быстрый обратный квадратный корень
Бы́стрый обра́тный квадра́тный ко́рень (также быстрый InvSqrt() или 0x5F3759DF по используемой «магической» константе, в десятичной системе 1 597 463 007) — это быстрый приближённый алгоритм вычисления обратного квадратного корня для положительных 32-битных чисел с плавающей запятой. Алгоритм использует целочисленные операции «вычесть» и «битовый сдвиг», а также дробные «вычесть» и «умножить» — без медленных операций «разделить» и «квадратный корень». Несмотря на «хакерство» на битовом уровне, приближение монотонно и непрерывно: близкие аргументы дают близкий результат. Точности (менее 0,2 % в меньшую сторону и никогда — в большую)[1][2] не хватает для настоящих численных расчётов, однако вполне достаточно для трёхмерной графики.
Алгоритм
Алгоритм принимает 32-битное число с плавающей запятой (одинарной точности в формате IEEE 754) в качестве исходных данных и производит над ним следующие операции:
- Трактуя 32-битное дробное число как целое, провести операцию y0 = 5F3759DF16 − (x >> 1), где >> — битовый сдвиг вправо. Результат снова трактуется как 32-битное дробное число.
- Для уточнения можно провести одну итерацию метода Ньютона: y1 = y0(1,5 − 0,5xy0²).
Реализация из Quake III[3]:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
Эта реализация считает, что float по длине равен long, и использует для преобразования указатели (может ошибочно сработать оптимизация «если изменился float, ни один long не менялся»; на GCC при компиляции в «выпуск» срабатывает предупреждение). По комментариям видно, что Джон Кармак, выкладывая игру в открытый доступ, не понял, что там делается.
Корректная по меркам современного Си реализация, с учётом возможных оптимизаций и кроссплатформенности:
#include <stdint.h>
float Q_rsqrt( float number )
{
const float x2 = number * 0.5F;
const float threehalfs = 1.5F;
union {
float f;
uint32_t i;
} conv = {number}; // member 'f' set to value of 'number'.
conv.i = 0x5f3759df - ( conv.i >> 1 );
conv.f *= threehalfs - x2 * conv.f * conv.f;
return conv.f;
}
</stdint.h>
На Си++20 можно использовать новую функцию bit_cast.
#include <bit>
#include <limits>
#include <cstdint>
constexpr float Q_rsqrt(float number) noexcept
{
static_assert(std::numeric_limits<float>::is_iec559); // Проверка совместимости целевой машины
float const y = std::bit_cast<float>(
0x5f3759df - (std::bit_cast<std::uint32_t>(number) >> 1));
return y * (1.5f - (number * 0.5f * y * y));
}
</std::uint32_t></float></float></cstdint></limits></bit>
GCC и Clang (-std=c++20 -mx32 -O3) дают одинаковый машинный код для всех трёх вариантов и близкий — друг относительно друга. У MSVC (/std:c++20 /O2) третья функция незначительно отличается от первых двух.
История
Саму идею приближения дробного числа целым для вычисления придумали Уильям Кэхэн и К. Ын в 1986[4]. До этой идеи добрались Грег Уолш, Клив Моулер и Гэри Таролли, работавшие тогда в Ardent Computer[5][6]. Грегу Уолшу и приписывается знаменитая константа 0x5F3759DF.
Таролли, перешедший в 3dfx, принёс алгоритм туда, где он и применялся для расчёта углов падения и отражения света в трёхмерной графике. Джим Блинн, специалист по 3D-графике, переизобрёл метод в 1997 году с более простой константой[7]. Более сложный табличный метод, который считает до 4 знаков (0,01 %), найден при дизассемблировании игры Interstate ’76 (1997)[8].
Однако данный метод не появлялся на общедоступных форумах, таких как Usenet, до 2002—2003-х годов. Метод обнаружили в Quake III: Arena и приписали авторство Джону Кармаку. Тот предположил, что его в id Software принёс Майкл Абраш, специалист по графике, или Терье Матисен, специалист по ассемблеру; другие ссылались на Брайана Хука, выходца из 3dfx[9]. Изучение вопроса показало, что код имел более глубокие корни как в аппаратной, так и в программной сферах компьютерной графики. Исправления и изменения производились как Silicon Graphics, так и 3dfx Interactive, при этом самая ранняя известная версия написана Гэри Таролли для SGI Indigo.
С выходом в свет в 1998 году набора инструкций 3DNow! в процессорах фирмы AMD появилась ассемблерная инструкция PFRSQRT[10] для быстрого приближенного вычисления обратного квадратного корня. Версия для double бессмысленна — точность вычислений не увеличится[2] — потому её не добавили. В 2000 году в SSE2 добавили функцию RSQRTSS[11] более точную, чем данный алгоритм (0,04 % против 0,2 %). Потому данный метод больше не имеет смысла в современных компьютерах[12] (все x64-процессоры поддерживают SSE2), зато важен как дань истории и в более ограниченных машинах[13].
Анализ и погрешность
Битовое представление 4-байтового дробного числа в формате IEEE 754 выглядит так:
| Знак | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Порядок | Мантисса | |||||||||||||||||||||||||||||||
| 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| 31 | 24 | 23 | 16 | 15 | 8 | 7 | 0 | |||||||||||||||||||||||||
Имеем дело только с положительными числами (знаковый бит равен нулю), не денормализованными, не ∞ и не NaN. Такие числа в стандартном виде записываются как 1,mmmm2·2e. Часть 1,mmmm называется мантиссой, e — порядком. Головную единицу не хранят (неявная единица), так что величину 0,mmmm назовём явной частью мантиссы. Кроме того, у машинных дробных чисел смещённый порядок: 20 записывается как 011.1111.12.
На положительных числах биекция «дробное ↔ целое» (ниже обозначенная как ) непрерывна как кусочно-линейная функция и монотонна. Отсюда сразу же можно заявить, что быстрый обратный корень, как комбинация непрерывных функций, непрерывен. А первая его часть — сдвиг-вычитание — к тому же монотонна и кусочно-линейна. Биекция сложна, но почти «бесплатна»: в зависимости от архитектуры процессора и соглашений вызова, нужно или ничего не делать, или переместить число из дробного регистра в целочисленный.
Например, двоичное представление 16-ричного целого числа 0x5F3759DF есть 0|101.1111.0|011.0111.0101.1001.1101.11112 (точки — границы полубайтов, вертикальные линии — границы полей компьютерного дробного). Порядок 101 1111 02 равен 19010, после вычитания смещения 12710 получаем показатель степени 6310. Явная часть мантиссы 01 101 110 101 100 111 011 1112 после добавления неявной ведущей единицы превращается в 1,011 011 101 011 001 110 111 112 = 1,432 430 148…10. С учётом реальной точности компьютерных дробных 0x5F3759DF ↔ 1,432430110·263.
Обозначим явную часть мантиссы числа , — несмещённый порядок, — разрядность мантиссы, — смещение порядка. Число , записанное в линейно-логарифмической разрядной сетке компьютерных дробных, можно[14][3] приблизить логарифмической сеткой как , где — параметр, используемый для настройки точности приближения. Этот параметр варьируется от 0 (формула точна при и ) до 0,086 (точна в одной точке, )
Воспользовавшись этим приближением, целочисленное представление числа можно приблизить как
Соответственно, .
Проделаем это же[3] для (соответственно ), и получим
Магическая константа , с учётом границ , в арифметике дробных чисел будет иметь несмещённый порядок и мантиссу ), а в двоичной записи — 0|101.1111.0|011… (1 — неявная единица; 0,5 пришли из порядка; маленькая единица соответствует диапазону [1,375; 1,5) и потому крайне вероятна, но не гарантирована нашими прикидочными расчётами.)
Можно вычислить, чему равняется первое кусочно-линейное приближение[15] (в источнике используется не сама мантисса, а её явная часть ):
- Для : ;
- Для : ;
- Для : .
На бо́льших или меньших результат пропорционально меняется: при учетверении результат уменьшается ровно вдвое.
Метод Ньютона даёт[15] , , и . Функция убывает и выпукла вниз, на таких функциях метод Ньютона подбирается к истинному значению слева — потому алгоритм всегда занижает ответ.
После одного шага метода Ньютона результат получается довольно точный (+0 % −0,18 %)[1][2], что для целей компьютерной графики более чем подходит (1⁄256 ≈ 0,39 %). Такая погрешность сохраняется на всём диапазоне нормированных дробных чисел. Два шага дают точность в 5 цифр[1], после четырёх достигается погрешность double.
Метод Ньютона не гарантирует монотонности, но компьютерный перебор показывает, что монотонность всё-таки есть.
Исходный текст (C++)
Ненормативная лексика
Материал содержит ненормативную лексику
Ненормативная лексика
Эта страница или раздел содержит ненормативную лексику
Ненормативная лексика
Материал содержит ненормативную лексику
Материал содержит ненормативную лексику
Эта страница или раздел содержит ненормативную лексику
#include <iostream>
union FloatInt {
float asFloat;
int32_t asInt;
};
int floatToInt(float x)
{
FloatInt r;
r.asFloat = x;
return r.asInt;
}
float intToFloat(int x)
{
FloatInt r;
r.asInt = x;
return r.asFloat;
}
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i // i don't know, what the fuck!
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
return y;
}
int main()
{
int iStart = floatToInt(1.0);
int iEnd = floatToInt(4.0);
std::cout << "Numbers to go: " << iEnd - iStart << std::endl;
int nProblems = 0;
float oldResult = std::numeric_limits<float>::infinity();
for (int i = iStart; i <= iEnd; ++i) {
float x = intToFloat(i);
float result = Q_rsqrt(x);
if (result > oldResult) {
std::cout << "Found a problem on " << x << std::endl;
++nProblems;
}
}
std::cout << "Total problems: " << nProblems << std::endl;
return 0;
}
</float></iostream>
Существуют аналогичные алгоритмы для других степеней, например, квадратного или кубического корня[3].
Мотивация
«Прямое» наложение освещения на трёхмерную модель, даже высокополигональную, даже с учётом закона Ламберта и других формул отражения и рассеивания, сразу же выдаст полигональный вид — зритель увидит разницу в освещении по рёбрам многогранника[16]. Иногда так и нужно — если предмет действительно угловатый. А для криволинейных предметов поступают так: в трёхмерной программе указывают, острое ребро или сглаженное[16]. В зависимости от этого ещё при экспорте модели в игру по углам треугольников вычисляют нормаль единичной длины к криволинейной поверхности. При анимации и поворотах игра преобразует эти нормали вместе с остальными трёхмерными данными; при наложении освещения — интерполирует по всему треугольнику и нормализует (доводит до единичной длины).
Чтобы нормализовать вектор, надо разделить все три его компонента на длину. Или, что лучше, умножить их на величину, обратную длине: . За секунду должны вычисляться миллионы этих корней. До того как было создано специальное аппаратное обеспечение для обработки трансформаций и освещения, программное обеспечение вычислений могло быть медленным. В частности, в начале 1990-х, когда код был разработан, большинство вычислений с плавающей запятой отставало по производительности от операций с целыми числами.
Quake III Arena использует алгоритм быстрого обратного квадратного корня для ускорения обработки графики центральным процессором, но с тех пор алгоритм уже был реализован в некоторых специализированных аппаратных вершинных шейдерах, используя специальные программируемые матрицы (FPGA).
Даже на компьютерах 2010-х годов, в зависимости от загрузки дробного сопроцессора, скорость может быть втрое-вчетверо выше, чем с использованием стандартных функций[15].
Дальнейшие улучшения
При желании можно перебалансировать погрешность, умножив коэффициенты 1,5 и 0,5 на 1,0009, чтобы метод давал симметрично ±0,09 % — так поступили в игре Interstate ’76, которая также делают итерацию метода Ньютона[8].
Неизвестно, откуда взялась константа 0x5F3759DF ↔[a] 1,4324301·263. Перебором Крис Ломонт и Мэттью Робертсон выяснили[1][2], что наилучшая по предельной относительной погрешности константа[b] для float — 0x5F375A86 ↔ 1,4324500·263, для double — 0x5FE6EB50C7B537A9. Правда, для double алгоритм бессмысленный (не даёт выигрыша в точности по сравнению с float)[2]. Константу Ломонта удалось получить и аналитически (c = 1,432450084790142642179)[b], но расчёты довольно сложны[15][2].
Чех Ян Ка́длец двоичным поиском, а затем перебором в окрестности того, что поиск дал, нашёл алгоритм в 1,3 раза[c] более точный при той же вычислительной сложности — ±0,065 %[17]:
float inv_sqrt(float x)
{ union { float f; uint32 u; } y = {x};
y.u = 0x5F1FFFF9ul - (y.u >> 1);
return 0.703952253f * y.f * (2.38924456f - x * y.f * y.f);
}
Вместо метода Ньютона можно использовать метод Галлея, в данной задаче эквивалентный методу Ньютона для уравнения . Он точнее одного шага метода Ньютона, но не дотягивает до двух и требует деление[17]:
где нужно рассчитать всего один раз и сохранить во временной переменой.
Комментарии
Примечания
Ссылки
- C. Lomont, Fast inverse square root, Technical Report, 2003.
- A Brief History of InvSqrt by Matthew Robertson
- 0x5f3759df, further investigations into accuracy and generalizability of the algorithm by Christian Plesner Hansen