В этой статье мы поговорим о «магической» константе 0x5f3759df, лежащей в основе элегантного алгоритмического трюка для быстрого вычисления обратного квадратного корня.

Вот полная реализация этого алгоритма:

float FastInvSqrt(float x) {
  float xhalf = 0.5f * x;
  int i = *(int*)&x;  // представим биты float в виде целого числа
  i = 0x5f3759df - (i >> 1);  // какого черта здесь происходит ?
  x = *(float*)&i;
  x = x*(1.5f-(xhalf*x*x));
  return x;
}

Этот код вычисляет некоторое (достаточно неплохое) приближение для формулы

image

Сегодня данная реализация уже хорошо известна, и стала она такой после появления в коде игры Quake III Arena в 2005 году. Её создание когда-то приписывали Джону Кармаку, но выяснилось, что корни уходят намного дальше – к Ardent Computer, где в середине 80-ых её написал Грег Уолш. Конкретно та версия кода, которая показана выше (с забавными комментариями), действительно из кода Quake.
В этой статье мы попробуем разобраться с данным хаком, математически вывести эту самую константу и попробовать обобщить данный метод для вычисления произвольных степеней от -1 до 1.

Да, понадобится немного математики, но школьного курса будет более, чем достаточно.

Зачем?


Зачем вообще нам может понадобиться считать обратный квадратный корень, да ещё и пытаться делать это настолько быстро, что нужно реализовывать для этого специальные хаки? Потому, что это одна из основных операций в 3D программировании. При работе с 3D графикой используются нормали поверхностей. Это вектор (с тремя координатами) длиной в единицу, который нужен для описания освещения, отражения и т.д. Таких векторов нужно много. Нет, даже не просто много, а МНОГО. Как мы нормализуем (приводим длину к единице) вектор? Мы делим каждую координату на текущую длину вектора. Ну или, перефразируя, умножаем каждую координату вектора на величину:

image

Расчёт image относительно прост и работает быстро на всех типах процессоров. А вот расчёт квадратного корня и деление – дорогие операции. И вот поэтому – встречайте алгоритм быстрого извлечения обратного квадратного корня — FastInvSqrt.

Что он делает?


Что же делает вышеуказанная функция для вычисления результата? Она состоит из четырёх основных шагов. Первым делом она берёт входное число (которое пришло нам в формате float) и интерпретирует его биты как значение новой переменной i типа integer (целое число).

int i = *(int*)&x;         // представим биты float в виде целого числа

Далее над полученным целым числом выполняется некоторая целочисленная арифметическая операция, которая работает достаточно быстро и даёт нам некоторую аппроксимацию требуемого результата

i = 0x5f3759df - (i >> 1);  // какого черта здесь происходит ?

То, что мы получили в результате данной операции, ещё не является, собственно, результатом. Это лишь целое число, биты которого представляют некоторое другое число с плавающей запятой, которое нам нужно. А значит, необходимо выполнить обратное преобразование int во float.

x = *(float*)&i;

И, наконец, выполняется одна итерация метода Ньютона для улучшения аппроксимации.

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

И вот теперь у нас имеется отличная аппроксимация операции извлечения обратного квадратного корня. Последняя часть алгоритма (метод Ньютона) – достаточно тривиальная вещь, я не буду на этом останавливаться. Ключевой частью алгоритма является шаг №2: выполнение некоторой хитрой арифметической операции над целым числом, полученным от интерпретации битов числа с плавающей запятой в качестве содержимого типа int. На этом мы и сфокусируемся.

Какого черта здесь происходит?


Для понимания дальнейшего текста нам нужно вспомнить формат, в котором хранятся в памяти числа с плавающей запятой. Я опишу здесь только то, что важно здесь для нас (остальное всегда можно подсмотреть в Википедии). Число с плавающей запятой хранится как комбинация трёх составляющих: знака, экспоненты и мантиссы. Вот биты 32-битного числа с плавающей запятой:

image

Первым идёт бит знака, дальше 8 битов экспоненты и 23 бита мантиссы. Поскольку мы здесь имеем дело с вычислением квадратного корня, то я предположу, что мы будем иметь дело лишь с положительными числами (первый бит всегда будет 0).

Рассматривая число с плавающей запятой как просто набор битов, экспонента и мантисса могут восприниматься как просто два положительных целых числа. Давайте обозначим их, соответственно, Е и М (поскольку дальше мы будем часто на них ссылаться). С другой стороны, интерпретируя биты числа с плавающей запятой, мы будем рассматривать мантиссу как число между 0 и 1, т.е. все нули в мантиссе будут означать 0, а все единицы – некоторое число очень близкое (но всё же не равное) 1. Ну и вместо того, чтобы интерпретировать экспоненту как беззнаковое 8-битное целое число, давайте вычтем смещение (обозначим его как В), чтобы получить знаковое целое число в диапазоне от -127 до 128. Давайте обозначим float-интерпретацию этих значений как е и m. Чтобы не путаться, будем использовать прописные обозначения (Е, М) для обозначения целочисленных значений и строчные (е, m) – для обозначения чисел с плавающей запятой (float).

Преобразование из одного в другое тривиально:

image

image

В этих формулах для 32-битных чисел L = 2^23, а В = 127. Имея некоторые значения e и m, можно получить само представляемое ими число:

image

и значение соответствующей им целочисленной интерпретации числа:

image

Теперь у нас есть почти все кусочки паззла, которые нужны для объяснения “хака” в коде выше. Итак, нам на вход приходит некоторое число х и требуется рассчитать его обратный квадратный корень:

image

По некоторым причинам, которые вскоре станут понятны, я начну с того, что возьму логарифм по основанию 2 от обеих частей данного уравнения:

image

Поскольку числа, с которыми мы работаем, на самом деле являются числами с плавающей запятой, мы можем представить х и у согласно вышеуказанной формуле представления таких чисел:

image

Ох уж эти логарифмы. Возможно, вы не пользовались ими со школьных времён и немного подзабыли. Не волнуйтесь, немного дальше мы избавимся от них, но пока что они нам ещё нужны, так что давайте посмотрим, как они здесь работают.
В обеих частях уравнения у нас находится выражение вида:

image

где v находится в диапазоне от 0 до 1. Можно заметить, что для v от 0 до 1 эта функция достаточно близка к прямой линии:

image

Ну или в виде выражения:

image

Где ? – некоторая константа. Это не идеальное приближение, но мы можем попытаться подобрать ? таким образом, чтобы оно было достаточно неплохим. Теперь, использовав его, мы можем преобразовать вышеуказанное равенство с логарифмами в другое, уже не строго равное, но достаточно близкое и, самое главное, СТРОГО ЛИНЕЙНОЕ выражение:

image

Это уже что-то! Теперь самое время прекратить работать с представлениями в виде чисел с плавающей запятой и перейти к целочисленному представлению мантиссы и экспоненты:

image

Выполнив ещё несколько тривиальных преобразований (можно пропустить детали), мы получим нечто, выглядящее уже довольно знакомо:

image

image

image

Посмотрите внимательно на левую и правую части последнего уравнения. Как мы видим, у нас получилось выражение целочисленной формы требуемого значения y, выраженное через линейного вида формулу, включающую целочисленное представление значения х:

image

Говоря простыми словами: “y (в целочисленной форме) – это некоторая константа минус половина от целочисленной формы х”. В виде кода это:

i = K - (i >> 1);

Очень похоже на формулу в коде функции в начале статьи, правда?

Нам осталось найти константу K. Мы уже знаем значения B и L, но ещё не знаем чему равно ?.
Как вы помните, ? – это некоторое “поправочное значение”, которое мы ввели для улучшения аппроксимации функции логарифма к прямой линии на отрезке от 0 до 1. Т.е. мы можем подобрать это число сами. Я возьму число 0.0450465, как дающее неплохое приближение и использованное в оригинальной реализации. Используя его, мы получим:

image

Угадаете, как число 1597463007 представляется в HEX? Ну, конечно, это 0x5f3759df. Ну, так и должно было получиться, поскольку я выбрал ? таким образом, чтобы получить именно это число.

Таким образом, данное число не является битовой маской (как думают некоторые люди просто потому, что оно записано в hex-форме), а результатом вычисления аппроксимации.

Но, как сказал бы Кнут: “Мы пока что лишь доказали, что это должно работать, но не проверили, что это действительно работает”. Чтобы оценить качество нашей формулы, давайте нарисуем графики вычисленного таким образом значения обратного квадратного корня и настоящей, точной его реализации:

image

График построен для чисел от 1 до 100. Неплохо, правда? И это никакая не магия, не фокус, а просто правильное использование несколько, возможно, экзотичного трюка с представлением чисел с плавающей запятой в виде целочисленных и наоборот.

Но и это ещё не всё!


Все вышеуказанные преобразования и выражения дали нам не только объяснение константы 0x5f3759df, но и ещё несколько ценных выводов.

Во-первых, поговорим о значениях чисел L и B. Они определяются не нашей задачей по извлечению обратного квадратного корня, а форматом хранения числа с плавающей запятой. Это означает, что тот же трюк может быть проделан и для 64-битных и для 128-битных чисел с плавающей запятой – нужно лишь повторить вычисления для рассчета других констант.

Во-вторых, нам не очень-то и важно выбранное значение ?. Оно может не давать (да и на самом деле – не даёт) лучшую аппроксимацию функции x + ? к логарифму. ? выбрано таким, поскольку оно даёт лучший результат совместно с последующим применением алгоритма Ньютона. Если бы мы его не применяли, то выбор ? являлся бы отдельной интересной задачей сам по себе, эта тема раскрыта в других публикациях.

Ну и в конце-концов, давайте посмотрим на коэффициент “-1/2” в финальной формуле. Он получился таким из-за сути того, что мы хотели вычислить (“обратного квадратного корня”). Но, в общем, степень здесь может быть любой от -1 до 1. Если мы обозначим степень как р и обобщим все те же самые преобразования, то вместо “-1/2” мы получим:

image

Давайте положим р=0.5 Это будет вычисление обычного (не обратного) квадратного корня числа:

image
image

В виде кода это будет:

i = 0x1fbd1df5 + (i >> 1);

И что, это работает? Конечно, работает:

image

Это, наверное, хорошо известный способ быстрой аппроксимации значения квадратного корня, но беглый гуглинг не дал мне его названия. Возможно, вы подскажете?
Данный способ будет работать и с более “странными” степенями, вроде кубического корня:

image

image

Что в коде будет выражено, как:

i = (int) (0x2a517d3c + (0.333f * i));

К сожалению, из-за степени 1/3 мы не можем использовать битовые операции сдвига и вынуждены применить здесь умножение на 0.333f. Аппроксимация всё ещё достаточно хороша:

image

И даже более того!


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

image

То сможем вычислять требуемые значения на лету, для произвольной степени от -1 до 1:

i = (1 - p) * 0x3f7a3bea + (p * i);

Чуть упростив выражение, мы даже можем сэкономить на одном умножении:

i = 0x3f7a3bea + p * (i - 0x3f7a3bea);

Эта формула даёт нам “магическую” константу, с помощью которой можно рассчитывать на лету разные степени чисел (для степеней от -1 до 1). Для полного счастья нам не хватает лишь уверенности в том, что вычисленное таким образом приближенное значение может быть столь же эффективно улучшено алгоритмом Ньютона, как это происходило в оригинальной реализации для обратного квадратного корня. Я не изучал эту тему более глубоко и это, вероятно, будет темой отдельной публикации (наверное, не моей).

Выражение выше содержит новую “магическую константу” 0x3f7a3bea. В некотором смысле (из-за своей универсальности) она даже “более магическая”, чем константа в оригинальном коде. Давайте назовём её С и посмотрим на неё чуть внимательнее.

Давайте проверим работу нашей формулы для случая, когда p = 0. Как вы помните из курса математики, любое число в степени 0 равно единице. Что же будет с нашей формулой? Всё очень просто – умножение на 0 уничтожит второе слагаемое и у нас останется:

i = 0x3f7a3bea;

Что, действительно является константой и, будучи переведённой во float-формат, даст нам 0.977477 – т.е. “почти 1”. Поскольку мы имеем дело с аппроксимациями – это неплохое приближение. Кроме того, это говорит нам кое-что ещё. Наша константа С имеет совершенно не случайное значение. Это единица в формате чисел с плавающей запятой (ну или “почти единица”)
Это интересно. Давайте взглянем поближе:

Целочисленное представление C это:

image

Это почти, но всё же не совсем, форма числа с плавающей запятой. Единственная проблема – мы вычитаем вторую часть выражения, а должны были бы её прибавлять. Но это можно исправить:

image

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

image

А вот мантисса:

image

А, значит, значение самого числа будет равно:

image

И правда, если поделить наше ? (а оно было равно 0.0450465) на 2 и отнять результат от единицы, то мы получим уже известное нам 0.97747675, то самое, которое “почти 1”. Это позволяет нам посмотреть на С с другой стороны и вычислять его на рантайме:

float sigma = 0.0450465;
float c_sigma = 1 - (0.5f * sigma);
int C_sigma = *(*int)&c_sigma;

Учтите, что для фиксированного ? все эти числа будут константами и компилятор сможет рассчитать их на этапе компиляции. Результатом будет 0x3f7a3beb, что не совсем 0x3f7a3bea из расчетов выше, но отличается от него всего на 1 бит (наименее значимый). Получить из этого числа оригинальную константу из кода (и заголовка данной статьи) можно, умножив результат вычислений ещё на 1.5.

Со всеми этими выкладками мы приблизились к пониманию того, что в коде в начале статьи нет никакой “магии”, “фокусов”, “интуиции”, “подбора” и прочих грязных хаков, а есть лишь чистая математика, во всей своей первозданной красоте. Для меня главным выводом из этой истории стала новость о том, что преобразование float в int и наоборот путем переинтерпретации одного и того же набора бит – это не ошибка программиста и не “хак”, а вполне себе иногда разумная операция. Слегка, конечно, экзотическая, но зато очень быстрая и дающая практически полезные результаты. И, мне кажется, для этой операции могут быть найдены и другие применения – будем ждать.

Комментарии (109)


  1. spmbt
    22.08.2017 13:20
    -4

    > 0x5f3759df (это название статьи)
    Содержание — супер.
    … А в плане SEO название статьи — предельно неудачное: ) Когда клоны наедятся — стоит переименовать. Может, так и было задумано?


    1. tangro Автор
      22.08.2017 13:43
      +34

      Это название оригинальной статьи. Ради какого-то там SEO портить авторскую задумку — несправедливо.


      1. spmbt
        22.08.2017 16:30

        Но SEO в данном случае я видел «в благородном смысле» — ради пользования. Читатель ни за что не запомнит название статьи и не введёт в поиск. Значит, возможность SEO для него пропала. Если бы статья называлась "Быстрый алгоритм квадратного корня и 0x5f3759df" — не пострадали бы ни поиск, ни задумка автора.

        (Или, хуже вариант — «Зачем 0x5f3759df для квадратного корня»)


        1. RadicalDreamer
          26.08.2017 00:21

          Читатель ни за что не запомнит название статьи и не введёт в поиск.

          Поисковый запрос вроде «магическая константа обратный корень» вполне себе нормально гуглится.


  1. Halt
    22.08.2017 13:50
    +18

    Со всеми этими выкладками мы приблизились к пониманию того, что в коде в начале статьи нет никакой “магии”, “фокусов”, “интуиции”, “подбора” и прочих грязных хаков, а есть лишь чистая математика, во всей своей первозданной красоте. Для меня главным выводом из этой истории стала новость о том, что преобразование float в int и наоборот путем переинтерпретации одного и того же набора бит – это не ошибка программиста и не “хак”, а вполне себе иногда разумная операция.
    Ошибаетесь. В этом коде есть чудовищный и катастрофический хак, который просто обязательно надо было упомянуть. А именно, нарушение правила strict aliasing-а при разадресации указателя. Все беды в коде возникают оттого, что кто-то проникся «элегантным решением», не разобравшись во всех деталях, а потом начинает применять его к месту и не к месту.

    Эту и другие темы я разбирал в своем докладе на конференции C++ Russia. Кому интересно, можете заглянуть.


    1. Halt
      22.08.2017 13:58
      +8

      Подробный разбор по strict aliasing-у можно найти в тексте другой моей статьи.


    1. erlyvideo
      23.08.2017 06:53

      а что поделать, тут пользуются аппаратным ускорением логарифмирования


      1. Halt
        23.08.2017 07:14
        +8

        Вопрос не в том, пользуются или не пользуются. Любое трюкачество должно быть уместно. А если его таки применяют, должен быть большой жирный комментарий, почему оно здесь и какие условия нужны для того чтобы все работало, включая фазы луны и прочее.

        Код в статье будет работать, если компилятору передали ключ -fno-strict-aliasing, либо если эта опция умолчательная для компилятора.

        Если через n лет проект начнут портировать на другую архитектуру (или переедут на другой компилятор) и забудут про этот маленький трюк, последствия могут оказаться самыми печальными.


        1. tangro Автор
          23.08.2017 10:28

          Чтобы печальных последствий не было, достаточно 1-2 юнит-тестов. Если компилятор вдруг решит поменять размер int или стрюкачить с алиасингом — это произойдёт на этапе компиляции и легко отловится тестом.


          1. Halt
            23.08.2017 11:05
            +4

            К сожалению, этого не достаточно. Программа, содержащая неопределенное поведение — некорректна. Не существует никаких критериев, позволяющих доказать корректность программы, кроме формального анализа и ручного разбора ассемблера в данном конкретном случае. Даже компилятор сам не знает, есть ли там UB или нет.

            Поменяв одно выражение в любой части кода вы можете повлиять на инлайнинг и как следствие на порядок выполнения проходов компилятора. Вам не доводилось видеть ситуацию, когда в программе выполняются обе ветви условия одновременно? Это один из возможных вариантов реакции компилятора на UB.

            Выполняя оптимизации, компилятор исходит из предположения, что неопределенного поведения в программе нет. Соответственно абсолютно все действия, выполняемые компилятором, заботятся только об обеспечении наблюдаемого поведения. Код из статьи таковым не является.

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

            Резюмируя, юнит тест в таком случае даст не больше, чем комментарий «мамой клянусь, оно работает!». Даже хуже, проход тестов может вселить только ложную уверенность в том, что все хорошо.


            1. mayorovp
              23.08.2017 11:36

              assert в тесте как раз защитить можно — надо только модуль с тестами компилировать без оптимизации, и линковать с остальной программой динамически (или статически, но в два этапа — второй этап линковки уже без оптимизации).


              Хуже другое — UB может никак не проявляться в тестах, но проявиться уже в реальной программе. Вот тут и правда никак не закрыться.


              1. Halt
                23.08.2017 11:46
                +1

                Идя таким путем, вы вступаете на очень зыбкую почву. Неизвестно еще, что будет хуже.

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

                Другое дело, что вся эта машинерия может негативно сказаться на производительности и потерять в итоге смысл.

                Современные компиляторы достаточно умные, чтобы использовать весь набор доступных инструкций, включая уже названную rsqrtps. Главное просто не мешать и не считать себя умнее компилятора.


                1. mayorovp
                  23.08.2017 12:38

                  Так я потому и пишу: хуже другое — UB может никак не проявляться в тестах, но проявиться уже в реальной программе. Вот тут и правда никак не закрыться.


                  1. Halt
                    23.08.2017 13:46

                    Я видел. Это были мысли вслух, а не наезд :)


      1. apro
        24.08.2017 08:14
        +1

        а что поделать, тут пользуются аппаратным ускорением логарифмирования

        можно написать так:


        float FastInvSqrt(float x) {
          float xhalf = 0.5f * x;
          int32_t i;
          memcpy(&i, &x, sizeof(i));  // представим биты float в виде целого числа
          i = 0x5f3759df - (i >> 1);  // какого черта здесь происходит ?
          memcpy(&x, &i, sizeof(x));
          x = x*(1.5f-(xhalf*x*x));
          return x;
        }

        современный компилятор заинлайнит memcpy с фиксированным размером,
        и в результате будет тот же код, что и задумывался,
        зато ни UB, ни прочих непрятных эффектов


    1. F0iL
      23.08.2017 12:18

      Материалы по ссылкам прочёл, и даже понимание пришло в целом, но не могу понять, что конкретно не так в коде функции в статье? Типы примитивные, массивов нет, компилятор поместит i и x в регистры или на стеке… и что не так? Ради интереса скомпилировал тест с -O3 и -O0 с включенным и выключенным алиасингом на g++ и clang++ (std=c++14) — результат выполнения одинаков. В чем должен быть подвох?


      1. Halt
        23.08.2017 12:33
        +2

        что конкретно не так в коде функции в статье?
        Код в статье нарушает стандарт С++ и провоцирует UB. Дальше можно уже не разбираться, ибо на этом месте заканчиваются все гарантии.

        Типы примитивные, массивов нет, компилятор поместит i и x в регистры или на стеке… и что не так?
        Что именно компилятор будет делать со значениями зависит от сотни-другой параметров, включая оценку весов при инлайнинге и register pressure в данной точке. Сама попытка объяснить, что сделает компилятор из общих соображений и «здравого смысла» уже обречена на провал.

        Ради интереса скомпилировал тест с -O3 и -O0 с включенным и выключенным алиасингом на g++ и clang++ (std=c++14) — результат выполнения одинаков.
        То что он одинаков здесь и сейчас не значит, что он будет одинаков всегда. В этом и засада. Пример того, что может случиться, озвучивается в докладе в районе 28 и 29 слайдов.

        К сожалению, самое страшное, что может сделать программист в непонятной ситуации, — «это проверить на практике и убедиться, что все хорошо».


        1. F0iL
          23.08.2017 13:13
          +1

          То что это нарушение стандарта и UB — это с самого начала было понятно, интересно было как раз увидеть реальный пример «что конкретно может пойти не так», для этого, собственно, и пробовал скомпилировать с разными ключами, чтобы получить разные результаты и пройтись потом по отладочному выводу.


          1. mayorovp
            23.08.2017 13:16
            +1

            Так кидали же уже ссылку ниже...


          1. Halt
            23.08.2017 13:45

            Могу посоветовать две статьи: раз и два.


      1. mayorovp
        23.08.2017 12:35
        +1

        Код основан на трюке с обращением к float через указатель на int: int i = *(int*)&x;, x = *(float*)&i;. Вот это и есть "не так".


        Подвох в том, что эта операция — по стандарту UB (нарушение strict aliasing rule). Некоторые компиляторы имеют ключи для отключения этого правила или вообще не учитывают его при оптимизациях, потому код и работает. Но такой код является непереносимым не только на другие платформы, но даже на новую версию компилятора.


      1. lieff
        23.08.2017 12:36
        +1

        Подвох в том, что по стандарту это UB. Несмотря на то что указатель скастили вот только что — на указателях этих есть пометка, что они гарантированно не пересекаются (когда они гарантированно пересекаются), нарушается правило strict aliasing стандарта. Кстати в статье не указан еще один метод: пометить указатели как могущие пересекаться __attribute__((__may_alias__)), но это уже расширение.


    1. netch80
      23.08.2017 15:50

      В ряде источников (например, тут) сказано, что обходить проблему strict aliasing можно надёжно через union?ы — правда, сказано это для C, а не C++. У Вас утверждается, что union только условно безопасен. Кому верить и отчего это зависит?


      1. Halt
        23.08.2017 17:01

        На том же Stackoverflow есть хороший вопрос после которого понятно, что ничего непонятно.

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

        Но судя по изменениям, движемся мы в сторону разрешения type punning через union.


      1. khim
        24.08.2017 00:52
        +2

        Кому верить и отчего это зависит?
        От компилятора. GCC разрешает подобные трюки, правда с оговорками, clang — нет.

        Непонятно, впрочем, зачем всё это, если есть переносимая реализация:

        Смотрите сами
        $ cat cast.cc
        #include <string.h>
        
        int float_to_int(float x) {
          int result;
          memcpy(&result, &x, sizeof(float));
          return result;
        }
        khim@khim:~/work/android/android-standalone/prebuilts/clang/host/linux-x86/clang-4053586/bin$ ./clang++ -O3 -S cast.cc -o- | c++filt
                .text
                .file   "cast.cc"
                .globl  float_to_int(float)
                .p2align        4, 0x90
                .type   float_to_int(float),@function
        float_to_int(float):                      # @float_to_int(float)
                .cfi_startproc
        # BB#0:
                movd    %xmm0, %eax
                retq
        .Lfunc_end0:
                .size   float_to_int(float), .Lfunc_end0-float_to_int(float)
                .cfi_endproc
        
                .ident  "Android clang version 5.0.300080  (based on LLVM 5.0.300080)"
                .section        ".note.GNU-stack","",@progbits
        


        1. Halt
          24.08.2017 07:34
          +2

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

          Хотя, конечно, по-прежнему остается проблема разных размеров. Кэп подсказывает, что надо добавить ассерт, плюс использовать типы с фиксированным размером, вроде uint32_t.


          1. khim
            24.08.2017 19:29

            Главное не подпускать к коду тех, кто вопит про неэффективность и упущенную прибыль возможность оптимизации :)
            Нет никакой «упущенной прибыли». Даже самый последний MSVC научился с memcpy работать несколько лет назад. Покажите им сгененированный ассеблерный код — и они умолкнут.

            Кэп подсказывает, что надо добавить ассерт, плюс использовать типы с фиксированным размером, вроде uint32_t.
            Не бывает флоатов фиксированного размера в стандарте, увы. А вот static_assert — это да.


  1. Xandrmoro
    22.08.2017 13:51
    +4

    > Со всеми этими выкладками мы приблизились к пониманию того, что в коде в начале статьи нет никакой “магии”

    Скорее, наоборот, я ещё больше укрепился в том, что математика — это магия.


    1. windgrace
      22.08.2017 17:20
      +10

      Третий закон Кларка, однако.

      Any sufficiently advanced technology is indistinguishable from magic


  1. Arastas
    22.08.2017 13:52
    +2

    нам не хватает лишь уверенности в том, что вычисленное таким образом приближенное значение может быть столь же эффективно улучшено алгоритмом Ньютона

    Я тут на прикинул на бумажке. Получается, что для возведения x в степень p, где p не ноль, шаг по Ньютону будет image, где z это аргумент функции (который используется в оригинальном коде для вычисления xhalf). Похоже, что удобно будет только когда -1/p — положительное целое.


  1. Browning
    22.08.2017 14:27
    +18

    В блоке "интересные публикации" название статьи отображается переведённым в десятичную систему счисления. Exploitable?


    1. tangro Автор
      22.08.2017 16:09
      +1

      Вот это да. Ну, заодно и потестили Хабр.


    1. AngReload
      22.08.2017 16:34

      В TMFeed аналогично — «1597463007».


    1. michael_vostrikov
      22.08.2017 17:12

      Возможно там при рендеринге есть что-то типа:


      if (is_numeric($value)) echo (0 + $value);

      Работает на PHP 5.6


    1. tangro Автор
      22.08.2017 18:13
      +4

      Администрация Хабра переименовала статью :)
      Ну ок.


      1. sebres
        22.08.2017 19:18
        +3

        Интересно какие ксплойты повылазят, если опубликовать перевод к "0x5f3759df(appendix)"...


        ReferenceError: appendix is not defined?

        Ради какого-то там SEO портить авторскую задумку — несправедливо.

        +1
        Про заворот слепых кишок у SEO-шников опосля второй статьи я помолчу лучше...


  1. andy_p
    22.08.2017 14:28
    +8

    Хорошо, что на данной платформе sizeof(int) == sizeof(float).


    1. x893
      22.08.2017 14:41
      +4

      Это надо в начале алгоритма поставить как assert, иначе долго будут искать причину гуру программирования когда неправильно считать будет.


      1. khim
        22.08.2017 19:55
        +2

        С современными компиляторами этот код вообще использовать нельзя, нарушения стандарта — они такие, да. Компилятор вполне может «соптимизировать» весь этот код в «return 0».


        1. x893
          22.08.2017 21:04

          Что то меня терзают сомнения на тему таких умных компиляторов. Может конечно алгоритмы которые в шахматы выигрывают и скомпилируют в return 0, но те что в шашки — точно так не смогут. Да и отладчик с #pragma останется всегда. На крайняк и на ассемблере можно наколбасить. На нём уж точно замучаются оптимизировать.


          1. dkozh
            22.08.2017 22:08
            +1

            Good news, everyone.


            И хотя более новые версии GCC так (пока) не делают, такое поведение формально не противоречит стандарту, потому что в результате undefined behaviour может получиться все, что угодго (по факту оптимизатор решил, что (unsigned short *)&a и &a не алиасятся, и решил переставить инструкции местами, потому что почему бы и нет).


            1. vlanko
              22.08.2017 22:46

              такое чудо вылазит, начиная с О2
              А Clang сразу пишет 4, забив на 5.


        1. symbix
          23.08.2017 00:08
          -3

          Я ненастоящий сварщик, но — volatile проставить и все, не?


          1. agmt
            23.08.2017 11:37
            +2

            Не уверен, станет ли валиднее код, но медленнее — точно.


            1. netch80
              23.08.2017 15:44

              Доступ к памяти всё равно пройдёт через кэш, потеря будет в пару тактов на операции — это малосущественно.

              А ещё — для реализации под конкретный процессор можно вставить операцию местного ассемблера, и volatile не потребуется :)


          1. sebres
            23.08.2017 16:25
            -2

            volatile нужен platform-agnostic, multi-threaded ONLY.
            Грубо говоря не даст прочитать некоторому потоку (соптимизированное компилятором) промежуточное значение (явно не указанный программистом v1 = v2) или не даст вырезать (оптимизировать до 0 инструкций) неочевидную для компилятора установку такой переменной.
            Грубо говоря, он думает что переменная тут (или ниже) не используется (этим потоком), зато может вполне себе — другим.
            Т.е. конкретно тут — ни о чем...


            khim
            С современными компиляторами этот код вообще использовать нельзя
            Компилятор вполне может «соптимизировать» весь этот код в «return 0».

            С первым высказыванием согласиться могу, условно. Только не "нельзя" а не комильфо… И assert-ами обвязать желательно…
            Со вторым не согласен в корне — с какого это перепуга он его вырежет?
            В худшем случае UB (да и UB на касте int/float мне очень сомнительно)...


            1. netch80
              23.08.2017 16:39

              > Т.е. конкретно тут — ни о чем…

              Конкретно тут — о том, что компилятор обязан записать в память то, что пишут по указателю volatile, сразу, не задерживаясь; и прочитать по другому volatile указателю, не делая никаких предположений, что по этому указателю и почему.
              Этого достаточно, чтобы убрать проблему strict aliasing в компиляторе.

              > volatile нужен platform-agnostic, multi-threaded ONLY.

              К тредовости оно вообще-то отношения в C/C++ не имеет от слова «никак», пока пользуетесь стандартными механизмами вроде mutex lock/unlock.
              (Это отличается, кстати, от Java — где на volatile нагрузили синхронизацию доступа между тредами.)
              Компилятор не имеет права кэшировать чтение или запись переменной даже без volatile через вызов любой неизвестной компилятору функции с побочным эффектом изменения чего-то в памяти. Операции с мьютексами декларируются со свойством такого изменения, даже если они инлайнятся.

              > В худшем случае UB (да и UB на касте int/float мне очень сомнительно)…

              Таки может :(


              1. sebres
                23.08.2017 17:02
                -1

                К тредовости оно вообще-то отношения в C/C++ не имеет от слова «никак»

                Да ну? А вы про атомарные операции (конструкты, инструкции) что-нибудь слышали?
                Т.е. простейшие вещи (ака одна переменная типа int) вы правда мютексами защищаете?


                Мютекс кстати тут вообще никаким местом и именно что "от слова никак", ибо если явно не указан volatile, то мютекс вокруг вам в случае не очевидной (нежелательной) оптимизации не поможет "от слова ничем"…
                Например если компилятор выпилит установку переменной, как неиспользуемой далее.


                Про остальное же я промолчу, да…
                Например:


                Компилятор не имеет права кэшировать чтение или запись переменной даже без volatile через вызов любой неизвестной компилятору функции

                А то что эту функцию (неявно для компилятора) исполняет прямо сейчас другой поток, это куда?
                Я вам еще раз повторю volatile однопоточно не интересен (ну если не нативно какой либо контроллер с мапой переменная<->чего-нибудь железное, что сравнимо с многопоточностью, ибо есть еще один актер)...


                И да здесь не про Java...


                1. netch80
                  23.08.2017 17:08

                  > Да ну? А вы про атомарные операции (конструкты, инструкции) что-нибудь слышали?

                  «Мы» слышали. В том числе то, что атомарные переменные вообще-то не объявляют volatile, у них подобная функциональность заложена в штатные средства доступа.

                  > Т.е. простейшие вещи (ака одна переменная типа int) вы правда мютексами защищаете?

                  Давайте Вы внимательно прочтёте, на что отвечаете. А то такие Ваши домыслы, мягко говоря, удивляют.

                  > Например если компилятор выпилит установку переменной, как неиспользуемой далее.

                  Он не выпилит установку переменной, которая не локальная, потому что не следит за тем, кто её дальше будет использовать. Локальные для функции — они в таком обычно всё равно не участвуют (а если участвуют, это вообще-то не лучший стиль).

                  > А то что эту функцию (неявно для компилятора) исполняет прямо сейчас другой поток, это куда?

                  Какое отношение вообще имеет _исполнение_ функции в другом потоке? Или в Вашей обстановке у них при этом общие локальные данные?

                  > Я вам еще раз повторю volatile однопоточно не интересен

                  И я повторю, что он решает проблему со strict aliasing.

                  > Про остальное же я промолчу, да…

                  Настоятельно прошу вместо подобных «многозначительных» «умолчаний» вести обсуждение по сути.


                  1. sebres
                    23.08.2017 18:01

                    В том числе то, что атомарные переменные вообще-то не объявляют volatile
                    Давайте Вы внимательно прочтёте, на что отвечаете.

                    Вы про что вообще? Здесь вообще-то речь про int i (который атомарный) и про функцию FastInvSqrt (в которой ни в каком её месте не нужен volatile от того самого слова).


                    Настоятельно прошу… вести обсуждение по сути.

                    По сути: расскажите где в этом куске из трех строчек нужен volatile и почему и где "компилятор обязан записать в память сразу"...


                    float FastInvSqrt(float x) {
                      ...
                      int i = *(int*)&x;
                      i = 0x5f3759df - (i >> 1);
                      x = *(float*)&i;
                      ...
                    }

                    Даже если компилятор соптимизирует это совсем без использования памяти (т.е. будет использовать например регистр EAX для преобразования — он обязан следовать логике преобразования для каста как написано (а не как вы думаете).


                    Т.е. для того-же x86:


                    ; int i = *(int*)&x;
                    mov         eax,dword ptr [x]
                    ; i = 0x5f3759df - (i >> 1);
                    sar         eax,1
                    mov         ecx,5F3759DFh
                    sub         ecx,eax
                    ; x = *(float*)&i;
                    mov         dword ptr [x],ecx

                    ВСЕ!


                    Причем здесь вообще "strict aliasing"? Тип int (как и float) не являются структурами...


                    Если вы про это, почему mov dword ptr [x],ecx вместо например:


                    ; x = *(float*)&i;
                    mov         dword ptr [i],ecx
                    fld         dword ptr [i]
                    fstp        dword ptr [x]

                    То и это совсем из другой оперы и не имеет ни к volatile ни к "strict aliasing", ни даже к выравниванию никакого отношения.


                    Под UB же я имел ввиду как раз например если типы для платформы есть не равной длинны, и/или для этой платформы биты перевернуты LSB/MSB или little-endian vs. big-endian или подобное (однако и тут при желании оно лечится двумя строчками обернутыми в #if… #endif).


                    1. netch80
                      23.08.2017 19:00

                      > По сути: расскажите где в этом куске из трех строчек нужен volatile

                      Вы сузили контекст до конкретного куска кода. Этого не было в предыдущем обсуждении.

                      Если говорить об этом коде, то именно за счёт того, что x безусловно перетирается, проблемы нет.
                      Если же он был бы написан чуть-чуть иначе — например, было бы так

                        int i = *(int*)&x;
                        i = 0x5f3759df - (i >> 1);
                        *(int*)&x = i;
                      

                      (одна только последняя строчка поменялась)

                      то проблема уже, очень вероятно, вылезла бы в полный рост, — компилятор мог бы просто не заметить, что x изменено. (Мало ли кто пишет чего по постороннему адресу...)

                      И GCC начинает об этом предупреждать — мне он на неё дословно сказал

                      fsq3.cc:3:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
                         int i = *(int*)&x;  // представим биты float в виде цел
                                         ^
                      fsq3.cc:5:11: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
                         *(int*)&x = i;
                                 ^
                      


                      К вопросу о том, структуры это или нет, это не имеет ни малейшего отношения. Коллега Halt в соседних комментариях приводил пример (в видео), как это реально влияло на каком-то компиляторе — аналогом для данной функции было бы, что код свернулся до { return x; }

                      К этому, все Ваши реплики как
                      > он обязан следовать логике преобразования для каста как написано (а не как вы думаете).
                      > Тип int (как и float) не являются структурами…

                      относятся к совсем другим аспектам, но не к этому.

                      А вот volatile помогло бы именно за счёт того, что запись в *(int*)&x переписало бы независимо от того, что компилятор думает о том, как относятся x и i друг к другу — он бы просто обязан был держать обоих в памяти и не делать никаких предположений о том, что x не изменилось, раз он не видит данного изменения. И это даже для локальных переменных. В выходном коде видно, что они явно пишут в память (в стек) и тут же из неё читают. Именно об этом я говорил как о «паре лишних тактов».

                      К счастью, пока что (?) встреченные Clang и GCC во всех тестах сумели как-то опознать, что это одна и та же сущность в памяти. Но гарантировать это я бы не пытался.

                      Если хотите возражать — прошу это делать со ссылками на стандарты (на публичные final drafts, если иного источника нет). Любая отсылка вида «у меня всё работает» тут, увы, совершенно непригодна.


                      1. sebres
                        23.08.2017 19:42
                        +1

                        Вы сузили контекст до конкретного куска кода. Этого не было в предыдущем обсуждении.

                        Чего это? Велось обсуждение конкретного куска кода из статьи — ничего я не "сужал"!
                        Мы что обсуждаем-то и где?
                        Во вторых там выше черным по белому стоит "конкретно тут — ни о чем…".


                        Вы же влезли в эту ветку с доказательствами "О чем, еще как о чем!" А потом выясняется, что для какого-то отстраненного теоретического случая (который здесь ни к месту и ни про то вовсе).


                        К этому, все Ваши реплики как относятся к совсем другим аспектам, но не к этому.

                        (Подавившись) вы очень нахальны, молодой человек...


                        *(int*)&x = i;
                        то проблема уже, очень вероятно, вылезла бы в полный рост, — компилятор мог бы просто не заметить, что x изменено.

                        Да ну? А ничего что в данном случае его должно интересовать что i (а не x) изменено, и "не заметить" этого компилятор ну никак не сможет (сдается мне сударь, вы не совсем понимаете о чем здесь пишете)…
                        Действительно есть такие компиляторы, которые при использования подобного "тухлого" каста (ударение на подобного) имеют UB (который описан у них в доку и как минимум выкинет warning при агрессивной оптимизации), но...


                        Но во вторых и у ув. мистера Уолша и у мистера Кармака хватило ума не использовать такой "гнилой" каст, и они таки написали x = *(float*)&i; вместо вот этого вот *(int*)&x = i;.


                        Если хотите возражать — прошу это делать со ссылками на стандарты.

                        Я вам вовсе ничего не собираюсь не доказывать не возражать...


                        Для начала, что бы не быть голословным быть может вы мне приведете пример ("стандарт, публичные final drafts") где описан такой UB и на каком основании компилятор не заметит изменения "x" после даже такого "гнилого" каста и использует старое (не измененное) значение.


                        *(int*)&x = i;
                        x = x*(1.5f-(xhalf*x*x));


                        1. khim
                          24.08.2017 05:25
                          +1

                          Для начала, что бы не быть голословным быть может вы мне приведете пример («стандарт, публичные final drafts») где описан такой UB и на каком основании компилятор не заметит изменения «x» после даже такого «гнилого» каста и использует старое (не измененное) значение.


                          Да пожалуйста:
                          C99 – 6.5 Expressions

                          An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
                          • a type compatible with the effective type of the object,
                          • a qualified version of a type compatible with the effective type of the object,
                          • a type that is the signed or unsigned type corresponding to the effective type of the object,
                          • a a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
                          • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
                          • a character type.


                          И сноска: The intent of this list is to specify those circumstances in which an object may or may not be aliased.


                          То есть стандарт явно разрешает считать что значение x и значение *(int*)&x никак, совсем никак не связаны между собой. Равно как и значения i и *(int*)&i. Тем самым вторая строка в этом примере — не зависит от первой и от параметров функции. А четвёртая строка — не зависит от третьей. То есть пару из второй и третьей строки можно вынести куда угодно. C учётом того, что значение i, вычисленное в строке 4 ни на что не влияет вообще — можно эту пару строк и удалить…


                      1. sebres
                        23.08.2017 20:33

                        Да кстати о птичках...


                        GCC… warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

                        $ cd /tmp/
                        $ echo '#include <stdio.h>
                        
                        float FastInvSqrt(float x) {
                          float xhalf = 0.5f * x;
                          int i = *(int*)&x;
                          i = 0x5f3759df - (i >> 1);
                          x = *(float*)&i;
                          x = x*(1.5f-(xhalf*x*x));
                          return x;
                        }
                        
                        int main()
                        {
                          printf("fastinvsqrt of 9.0: %10.5f\n", FastInvSqrt(9.0));
                          return 0;
                        }
                        ' > test.c
                        $ gcc -Wstrict-aliasing test.c -o test
                        $ # и даже совсем усе-усе:
                        $ gcc -Wall -Wextra test.c -o test
                        $ ./test
                        fastinvsqrt of 9.0:    0.33295

                        Чисто для самообразования… На которой версии gcc (и/или платформе) вы видите такое?
                        А то я перепробовал от 3.4 до 6.2 (linux, win, и от отчаянья даже mac и solaris), нигде не увидел...


                        1. sebres
                          23.08.2017 20:39

                          Сорри за спам, забылось -O2 (вот блин что autoconf с людьми делает)…
                          Т.е. воспроизводится везде, начиная с 3.4…
                          Еще раз звиняюсь...


                        1. Scf
                          23.08.2017 20:43
                          +1

                          g++ -Wall -Wextra -O2 test.c -o test
                          test.c: In function ‘float FastInvSqrt(float)’:
                          test.c:5:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
                             int i = *(int*)&x;
                                             ^
                          test.c:7:17: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
                             x = *(float*)&i;

                          Правда, скомпилировать код так, чтобы он сломался, я не смог.
                          gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609


                          1. sebres
                            23.08.2017 20:48

                            Да нашел уже, см. выше
                            Звиняюсь ещё раз...


                          1. sebres
                            23.08.2017 21:52
                            +1

                            Покреативив вокруг да около, сделал чуть более "покладистый" вариант, вдруг кому буде нужно:


                            Example
                            $ echo '
                            #include <stdio.h>
                            #include <assert.h>
                            
                            #if defined(static_assert) || (__STDC_VERSION__ >= 201100L)
                            static_assert(sizeof(int) == sizeof(float), "Cannot cast types of different sizes (int/float)");
                            #else
                            #warning Unknown sizes of int/float to the compile time: assumed are equal (4/4).
                            #endif
                            
                            float FastInvSqrt(float x) {
                              char *buf = (char *)&x;
                              float xhalf = 0.5f * x;
                              int i = *(int*)buf;
                              i = 0x5f3759df - (i >> 1);
                              buf = (char *)&i;
                              x = *(float*)buf;
                              x = x*(1.5f-(xhalf*x*x));
                              return x;
                            }
                            
                            int main()
                            {
                              printf("fastinvsqrt of 9.0: %10.5f\n", FastInvSqrt(9.0));
                              return 0;
                            }
                            ' > test.c; gcc -Ofast -Wall -Wextra test.c -o test; ./test


                    1. khim
                      24.08.2017 05:15

                      Причем здесь вообще «strict aliasing»? Тип int (как и float) не являются структурами...
                      А какая разница? Strict aliasing гласит, что записать по адресу "(int*)&x" никак не может повлять на значение x!

                      он обязан следовать логике преобразования для каста как написано (а не как вы думаете).
                      С кастом — всё хорошо. С обращением в память — плохо.

                      В следующей программе:
                        float x;
                        float *q=&x
                        int *p=(int*)&x;
                        ...
                      
                      Компилятор имеет право считать, что запись через "*p" и чтение через "*q" (и наоборот) никак друг с другом не связаны. То же самое — в исходной программе. В строке x = *(float*)&i; x получает некоторое значение — но, с точки зрения компилятора, точно не имеющее никакого отношения к тому, что находится в i. Стало быть манипуляции с i — можно перенести ниже. А поскольку строка int i = *(int*)&x; инициализирует i каким-то значением, не имеющим отношения к тому, что находится в i — то это значение тоже можно вынести «вниз». А другая фаза того же компилятора может заметить, после всего этого, что мы пишем в x значение, которое ещё ничем не инициализировано вообще — ну так чего бы туда 0 не записать?


            1. khim
              24.08.2017 05:02
              +2

              Со вторым не согласен в корне — с какого это перепуга он его вырежет?
              С целью оптимизации кода. Код, который нифига не делает — точно короче того, который что-то делает.

              В худшем случае UB (да и UB на касте int/float мне очень сомнительно)...
              Тут не каст int/float. Тут Type punning. В результате компилятор может, ну, например, переставить работу с целыми числами до работы с плавучкой или после.

              То есть будет что-то подобное:
              float FastInvSqrt(float x) {
                int i;
                float xhalf = 0.5f * x;
                x = *(float*)&i;
                x = x*(1.5f-(xhalf*x*x));
                i = *(int*)&x;  // представим биты float в виде целого числа
                i = 0x5f3759df - (i >> 1);  // какого черта здесь происходит ?
                return x;
              }
              А после можно и «заметить» что мы обращаемся к неинициализированной переменной. Ну и раз мы всё равно можем вернуть что угодно — вернуть 0.


  1. ShinRa
    22.08.2017 16:13
    +3

    У меня дежавю, кажется что-то похожее я видел в хабе «Разработка игр».


    1. gresolio
      22.08.2017 18:47
      +3

      Упоминалось в переводе «Повышаем производительность кода: сначала думаем о данных».
      Также замечено в комментах: раз и два. Но перевода именно этой статьи раньше не было.


  1. alsii
    22.08.2017 16:55

    О! Еще один элегантный способ выяснить в матрице мы или нет? :-)


  1. Iqorek
    22.08.2017 17:22
    +4

    Есть какие то замеры производительности по сравнению с традиционным способом? С 2005 года и уж с тем более с 80х прошло много лет, может быть сегодня овчинка уже не стоит выделки?


    1. tangro Автор
      22.08.2017 18:13
      +1

      Что-то с 2005 года поменялось в теории сложности или архитектуре процессоров, что операции корня и деления стали работать быстрее суммирования и умножения? Вряд ли.


      1. khim
        22.08.2017 20:01
        +13

        Что-то с 2005 года поменялось в теории сложности или архитектуре процессоров
        Таки да


        1. tangro Автор
          22.08.2017 23:20

          Вот это да. И всё же вопрос был о сравнении с «традиционными способами», а вшитая в проц аппроксимация — это не оно.


        1. netch80
          23.08.2017 16:18

          Мнэээ…

          1. Таки нет, чисто формально, ибо RSQRTPS — команда SSE1, а это 1999, а не 2005 :)

          2. Вопрос был про «корня и деления» против «суммирования и умножения». Эти фокусы позволяют, например, ускорять деление через обратный корень, но с потерей точности — вот Intel про это пишет в 248966:

          > In microarchitectures that provide DIVPS/SQRTPS with high latency and low throughput, it is possible to speed up single-precision divide and square root calculations using the (V)RSQRTPS and (V)RCPPS instructions. For example, with 128-bit RCPPS/RSQRTPS at 5-cycle latency and 1-cycle throughput or with 256-bit implementation of these instructions at 7-cycle latency and 2-cycle throughput, a single Newton-Raphson iteration or Taylor approximation can achieve almost the same precision as the (V)DIVPS and (V)SQRTPS instructions.

          но всё равно это сильно дороже умножения…

          В этом плане лучше поставить на оптимизацию Goldschmidt method — AMD клянётся, что получает на делении до 16 циклов на single, 20 на double, 24 на extended — это заведомо быстрее типичного SRT. И наверняка там ещё можно что-то выжать. (А для квадратного корня ещё быстрее получается.)
          Обратная сторона — всё в плавучке — прибавлять 10 тактов на конверсию в обе стороны для целых это уже сравнимо с интеловским SRT…


          1. khim
            24.08.2017 05:35

            1. Таки нет, чисто формально, ибо RSQRTPS — команда SSE1, а это 1999, а не 2005 :)
            AMD K6-III выпускались точно до 2000го года, а компьютеры, я думаю, продавались и позже.

            но всё равно это сильно дороже умножения…
            Не сильно. На каком-нибудь K7 RSQRTPS вообще быстрее умножения. Можете в таблички посмотреть.

            На современных процессорах на оптимизацию подобных инструкций «забили», потому что кто же считает подобные вещи на CPU? Для этого GPU есть…


            1. netch80
              25.08.2017 09:25

              > На каком-нибудь K7 RSQRTPS вообще быстрее умножения.

              Смешно — но показывает странное направление работы AMD.

              > На современных процессорах на оптимизацию подобных инструкций «забили», потому что кто же считает подобные вещи на CPU? Для этого GPU есть…

              Ну, похоже, не совсем. Например, после Nehalem повырезали много идиотизмов работы с денормализованными (там, где наследие K-C-S тратило тысячи тактов). Как минимум в SSE оба вендора не игнорируют плавучку. Но это происходит медленнее, чем хотелось бы…


      1. ProLimit
        22.08.2017 23:00

        Есть же еще математические сопроцессоры (FPU). Например, в STM32 вычисление квадратного корня занимает 14 тактов, что не так много. Деление — еще 14. Так что если нужен обратный квадратный корень — лучше взять этот алгоритм, если прямой — лучше вычислить на FPU.


        1. rPman
          23.08.2017 01:29

          а теперь используйте сопроцессор в коде шейдера, например.


          1. chersanya
            23.08.2017 01:33
            +2

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


  1. LynXzp
    22.08.2017 17:41

    Вау! Прямо новая глава для Hacker's delight.


  1. Scf
    22.08.2017 20:06
    +6

    Шикарно!


    Поделюсь трюком, который я вычитал в исходниках Elite для zx spectrum: быстрое умножение двух однобайтных чисел.
    ab = (a^2 + b^2 - (a-b)^2)/2
    Эта формула элементарно выводится из школьного разложения квадрата разности. Т.е. можно быстро и точно умножать однобайтные числа, используя таблицу квадратов (512 байт)


    1. khim
      22.08.2017 20:31
      +8

      Это было реально круто на процессорах Z80 или 6502, на которых отсутствовала операция умножения. Современные же процессоры перемножают числа за 2-3 такта, так что необходимость в подобных трюках, в общем и целом, отпала…


      1. mayorovp
        23.08.2017 08:36
        +1

        Современные процессоры реализуют эту оптимизацию в железе, потому и так мало тактов требуется.


        1. netch80
          23.08.2017 15:25

          C современными возможностями по количеству вентилей вообще умножение строят на сетках и затем параллельных сумматорах… так что обычно не «эта» оптимизация. Хотя результат ещё лучше :)


      1. lieff
        23.08.2017 15:48
        +1

        Однако блоков умножения у современных процессоров обычно меньше чем ALU, и при out-of-order исполнении давление на эти блоки может оказаться высоким, а ALU блоки простаивать. Так что такие оптимизации все еще могут иметь смысл.


        1. khim
          24.08.2017 05:36

          Не имеют. Архитектура процессоров меняется настолько часто, что вы не успеете программу пересобирать под 100 разных архитектур…


          1. lieff
            24.08.2017 13:33

            Меняются то они часто, но вот принципиально блоков умножения больше чем блоков ALU не становится, т.к. эти блоки дорогие по площади кристалла. Вот например Skylake посмотрите https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)#Execution_engine
            ALU по прежнему больше и тому объективные причины, а не потому что не хочется.


    1. meta4
      22.08.2017 20:42
      +2

      Три дереференса + две суммы + одно вычитание + 1 побитовый сдвиг вместо одного умножения? оО


      1. ainoneko
        23.08.2017 10:38

        Там всегда a не меньше b?
        Иначе ещё нужна проверка. (Или я чего-то не понимаю?)


      1. alexeymalov
        23.08.2017 20:17

        Программная реализация умножения 8-битных чисел выполняется за пару сотен тактов на z80. умножение с использованием таблиц квадратов — несколько десятков тактов. При частоте процессора в 3,5 MHz ещё как ощутимо


    1. Iqorek
      22.08.2017 21:59

      я вычитал в исходниках Elite для zx spectrum

      Где где? Можно ссылку на исходники?


      1. Scf
        22.08.2017 22:13
        +8

        Ну это просто. Загружаешь MONS на 58500, запускаешь его. вбиваешь загрузчик кода Elite, он с адреса 5B00, длину блока указываешь так, чтобы MONS не перетереть. Потом переходишь на 7CB7, это точка входа в игру. И жмешь комбинацию клавиш для показа листинга. Вуаля — на экране исходник.


        20 лет прошло, до сих пор помню :-)


    1. netch80
      23.08.2017 15:20
      +1

      Есть более известный вариант:

      ab = (a+b)^2/4 - (a-b)^2/4
      


      только два лукапа по таблице, для a+b и a-b; деление на 4 выполняется нацело, дробные взаимно компенсируются.


      1. Scf
        23.08.2017 15:39

        Тогда a+b может быть больше 255, т.е. нужно оперировать словами и таблицу квадратов удваивать по размеру.


        1. netch80
          23.08.2017 15:55

          Да. Тут уже надо компромисс искать.
          Подозреваю, именно для Z80-based сокращение таблицы было важнее, но точными данными не владею.


  1. shukan
    22.08.2017 21:46
    +1

    Кто бы еще смог понять, что здесь происходит —

    Эвклид без Эвклида ?
    Понятно, что здесь происходит вычисление обратного числа, Y = X^-1
    Но почему именно так? Я хотел было призвать алгоритм Эвклида, но наткнулся на это!
    И почему для 64х бит достаточно всего лишь одной лишней итерации?
    И что за странный комментарий 3 bits correct? Это потому что odd?

    // modular inverse of odd 32-bit value
    uint32_t modinv32( uint32_t x ) {
            uint32_t y = x;      // 3 bits correct
            y *= 2 - x * y; // 6
            y *= 2 - x * y; // 12
            y *= 2 - x * y; // 24
            y *= 2 - x * y; // 32
            return y;
    }
    
    // modular inverse of odd 64-bit value
    uint64_t modinv64( uint64_t x ) {
            uint64_t y = modinv32( (uint32_t) x );
            y *= 2 - x * y;
            return y;
    }
    



    1. chersanya
      23.08.2017 01:07
      +3

      Это не алгоритм Евклида, а существенно более оптимальный, применимый для такого частного случая — получается сложность O(log k) вместо O(k) для Евклида. Переход можно обосновать так: если y = x^-1 mod 2^k, то xy = a 2^k + 1; домножим, как в вашей функции, на 2 - xy: получим xy * (2 - xy) = (a 2^k + 1) * (2 - a 2^k - 1) = -a^2 2^(2k) + a 2^k - a 2^k + 1 = 1 mod 2^(2k), то есть из обратного к х значения по модулю 2^k получили обратное по модулю 2^(2k). Делая такие переходы как раз из обратного по модулю 2^3 получим по 2^6 и т.д. Также отсюда ясно, почему для перехода к 64 битам достаточно одного шага.


    1. chersanya
      23.08.2017 09:22
      +2

      А, про первые 3 бита забыл написать. Тут можно очень просто убедиться, перебрав все нечётные числа от 1 до 7 (т.к. рассматриваем обратное по модулю 2^3 = 8) — как видно, каждое число является обратным самому себе. Ну и ещё — к чётным числам обратного ясное дело не существует, поэтому их вообще не рассматриваем.


  1. vlanko
    22.08.2017 22:37

    А в С++ в те времена int уже был 32-битным? Или это не имеет значения для указателей?


    1. tangro Автор
      22.08.2017 23:28
      +1

      В какие «те»? Quake III Arena, о котором идёт речь в статье, вышел в 1999 году, а int стал 32-битным (сначала это было даже опционально, в зависимости от модели памяти) с распространением 32-битных операционок, т.е. ещё на пару лет раньше.


      1. khim
        24.08.2017 05:39

        Quake III Arena, о котором идёт речь в статье, вышел в 1999 году, а int стал 32-битным (сначала это было даже опционально, в зависимости от модели памяти) с распространением 32-битных операционок, т.е. ещё на пару лет раньше.
        Какие, к терапевту, 32-битные операционки? 32-битный int — это Doom, 1993й год.


  1. janatem
    22.08.2017 22:58

    Только для денормализованных значений аргумента (близких к нулю) в результате, кажется, будет мусор, а не ожидаемое +inf.


    1. vlanko
      22.08.2017 23:32

      да, формула только для x>1


  1. askv
    22.08.2017 23:31

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


    1. mayorovp
      23.08.2017 08:39

      Как вы будете извлекать квадратный корень из числа с плавающей точкой без операций с плавающей точкой?


      1. askv
        23.08.2017 22:24

        Любое число с плавающей точкой f представляется в компе в виде f = z * 2^k. Его всегда легко привести к виду f = z' * 2^(2*k'). Тогда sqrt(f) = sqrt(z') * 2^k'.

        z, z', k, k' — целые. Таким образом, вычисление корня из f легко сводится к вычислению корня из целого числа z'.


        1. chersanya
          24.08.2017 00:01

          Ну а из целого числа как вы корень извлечёте? И ещё, чтобы такое провернуть нужно отдельно проводить операции над мантиссой и экспонентой, а процессоры хуже работают с числами нестандартного размера, да ещё и не выровненными.


          1. askv
            24.08.2017 06:27
            -2

            Из целого числа есть поразрядный алгоритм, наподобие умножения столбиком. В десятичной системе он сложный, после адаптации к двоичной превращается в двоичные побитовые сдвиги и сравнения. Погуглите «вычисление квадратного корня столбиком». Например, dxdy.ru/topic24572.html только нужно алгоритм из десятичной системы перевести в двоичную, он сильно упрощается.

            Причём тут невыровненные числа? В отдельные переменные (регистры) выделяется мантисса и экспонента, затем с каждыми из них операции ведутся как с обычными машинными словами. После вычислений float/double собирается обратно побитовыми операциями.


          1. bopoh13
            24.08.2017 15:20
            -2

            Если квадратный корень — целое число, то он является суммой нечетных чисел от 1 до 2n-1 (добавляем 2 порядка, от полученного значения убираем 1 порядок). Замерял скорость на скриптовых языках. Не впечатлён. Для C/C++ алгоритмов много.


  1. Keroro
    23.08.2017 06:26

    Для вычисления целочисленных корней на 8 бит PIC микроконтроллере давно использую Hardware algorithm [GLS] из hackersdelight. Очень компактный и шустрый.


    1. askv
      23.08.2017 06:37

      Есть ещё целочисленный алгоритм, там только побитовые сдвиги и сравнения, нет умножений. Когда-то писал на ассемблере, но листочек затерялся, не могу найти. Но можно попробовать воспроизвести.


  1. Aivendil
    23.08.2017 10:28
    +3

    Огромное спасибо! Ради таких статей я и пришёл в свое время на этот ресурс.


    1. netch80
      26.08.2017 16:56

      +1


  1. fishca
    25.08.2017 08:16

    Математика === Магия. Классная статья, спасибо!