Быстрый обратный квадрат двойного в C / C ++ - PullRequest
9 голосов
/ 16 марта 2012

Недавно я профилировал программу, в которой точка доступа определенно это

double d = somevalue();
double d2=d*d;
double c = 1.0/d2   // HOT SPOT

Значение d2 не используется после, потому что мне нужно только значение c. Некоторое время назад я читал о методе быстрого обратного квадратного корня Кармака, это явно не так, но мне интересно, могут ли подобные алгоритмы помочь мне вычислить 1 / x ^ 2.

Мне нужна довольно точная точность, я проверил, что моя программа не дает правильных результатов с опцией gcc -ffast-math. (Г ++ - 4.5)

Ответы [ 3 ]

19 голосов
/ 16 марта 2012

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

  1. Вы уверены, что вам нужна точность double?Вы можете пожертвовать точностью достаточно легко:

    double d = somevalue();
    float c = 1.0f / ((float) d * (float) d);
    

    В этом случае 1.0f абсолютно обязателен, если вы используете 1.0, вместо этого вы получите double точность.

  2. Вы пытались включить "небрежную" математику на вашем компиляторе?На GCC вы можете использовать -ffast-math, есть аналогичные опции для других компиляторов.Небрежная математика может быть более чем достаточно для вашего приложения.( Редактировать: Я не увидел никакой разницы в итоговой сборке.)

  3. Если вы используете GCC, рассматривали ли вы вопрос об использовании -mrecip?Существует функция «обратной оценки», которая имеет точность около 12 бит, но она намного быстрее.Вы можете использовать метод Ньютона-Рафсона, чтобы повысить точность результата.Опция -mrecip заставит компилятор автоматически сгенерировать для вас обратную оценку и шаги Ньютона-Рафсона, хотя вы всегда можете написать сборку самостоятельно, если хотите более точно настроить компромисс между точностью и производительностью.(Ньютон-Рафсон быстро сходится очень .) ( Редактировать: Мне не удалось получить GCC для генерации RCPSS. См. Ниже.)

Я нашел сообщение в блоге ( source ), в котором обсуждается точная проблема, с которой вы сталкиваетесь, и автор приходит к выводу, что такие методы, как метод Carmack, не могут конкурировать с инструкцией RCPSS (на которой установлен флаг -mrecip)GCC использует).

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

Трюки, которые не работают

  1. Метод Кармака: он устарел на современных процессорах, которые имеют коды обратной оценки.Для взаимных ссылок лучшая версия, которую я видел, дает только один бит точности - ничто по сравнению с 12 битами RCPSS.Я думаю, что это совпадение, что уловка работает так хорошо для взаимных квадратных корней;совпадение, которое вряд ли повторится.

  2. Перемаркировка переменных.Что касается компилятора, то разница между 1.0/(x*x) и double x2 = x*x; 1.0/x2 очень мала.Я был бы удивлен, если бы вы нашли компилятор, который генерирует различный код для двух версий с оптимизацией, включенной даже до самого низкого уровня.

  3. Использование pow.Библиотека pow - это монстр.При отключенном GCC -ffast-math вызов библиотеки довольно дорогой.При включенном -ffast-math в GCC вы получите точно такой же код сборки для pow(x, -2), что и для 1.0/(x*x), поэтому никаких преимуществ.

Обновление

Вот пример приближения Ньютона-Рафсона для обратного квадрата значения с плавающей запятой двойной точности.

static double invsq(double x)
{
    double y;
    int i;
    __asm__ (
        "cvtpd2ps %1, %0\n\t"
        "rcpss %0, %0\n\t"
        "cvtps2pd %0, %0"
        : "=x"(y)
        : "x"(x));
    for (i = 0; i < RECIP_ITER; ++i)
        y *= 2 - x * y;
    return y * y;
}

К сожалению, с помощью тестов RECIP_ITER=1 на моем компьютере он немного медленнее~ 5%), чем простая версия 1.0/(x*x).Это быстрее (в 2 раза быстрее) с нулевыми итерациями, но тогда вы получите только 12 бит точности.Я не знаю, достаточно ли для вас 12 битов.

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

Например, вы сказали, что -ffast-math вызвало нежелательную потерю точности;это может указывать на проблему числовой стабильности в алгоритме, который вы используете.При правильном выборе алгоритма многие проблемы можно решить с помощью float вместо double.(Конечно, вам может потребоваться более 24 бит. Я не знаю.)

Я подозреваю, что метод RCPSS срабатывает, если вы хотите вычислить несколько из них параллельно.

5 голосов
/ 16 марта 2012

Да, вы, конечно, можете попробовать что-нибудь.Позвольте мне дать вам некоторые общие идеи, вы можете заполнить детали.

Сначала давайте посмотрим, почему работает рут Кармака:

Мы пишем x = М × 2 E обычным способом.Теперь напомним, что число с плавающей запятой IEEE сохраняет смещение экспоненты смещением: если e обозначает поле экспоненты, мы имеем e = Bias + E ≥ 0. Переставляя, мы получаем E = e - Смещение.

Теперь для обратного квадратного корня: x −1 / 2 = M -1 / 2 × 2 - E / 2 .Новое поле экспоненты:

e ' = смещение - E / 2 = 3/2 смещение - e / 2

с переключением бит, мы можем получить значение e / 2 из e путем сдвига, а 3/2 Bias - это просто константа.

Более того, мантисса M сохраняется как 1.0 + x с x <1, и мы можем приблизительно <em>M -1 / 2 как 1 +х / 2.Опять же, тот факт, что только x хранится в двоичном виде, означает, что мы получаем деление на два простым сдвигом битов.


Теперь мы посмотрим на x -2 : это равно M -2 × 2 -2 E , и мы ищем поле экспоненты:

e ' = Смещение - 2 E = 3 Смещение - 2 e

Опять же, 3 Bias - это просто константа, и вы можете получить 2 e из e путем сдвига битов.Что касается мантиссы, вы можете приблизительно (1 + x) -2 на 1 - 2 x , и поэтому проблема сводится к получению 2 x от x .


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

1 голос
/ 16 марта 2012

Для вашей текущей программы вы определили точку доступа - хорошо.В качестве альтернативы ускорению 1 / d ^ 2 у вас есть возможность изменить программу, чтобы она не вычисляла 1 / d ^ 2 так часто.Можете ли вы поднять его из внутренней петли?Для скольких различных значений d вы вычисляете 1 / d ^ 2?Не могли бы вы предварительно рассчитать все необходимые значения, а затем посмотреть результаты?Это немного громоздко для 1 / d ^ 2, но если 1 / d ^ 2 является частью некоторого большего фрагмента кода, возможно, стоит применить этот прием к этому.Вы говорите, что если вы снизите точность, вы не получите достаточно хороших ответов.Есть ли способ перефразировать код, который может обеспечить лучшее поведение?Численный анализ является достаточно тонким, поэтому, возможно, стоит попробовать несколько вещей и посмотреть, что произошло.

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

...