Быстрый обратный квадрат double в C / C++

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

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

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

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

3 ответов


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

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

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

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

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

  3. если вы используете GCC, вы рассматривали использование -mrecip? Существует функция" взаимной оценки", которая имеет только около 12 бит точности, но она намного быстрее. Вы можете использовать метод Ньютона-Рафсона для повышения точности результат. The -mrecip опция заставит компилятор автоматически генерировать взаимную оценку и шаги Ньютона-Рафсона для вас, хотя вы всегда можете написать сборку самостоятельно, если хотите точно настроить компромисс производительности и точности. (Ньютон-Рафсон сходится!--42-->очень быстро.) ( Edit: я не смог заставить GCC генерировать RCPSS. Увидеть ниже.)

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

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

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

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

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

  3. используя pow. The pow функция библиотеки-это монстр. С GCC -ffast-math выключен, вызов библиотеки довольно дорогой. С GCC -ffast-math включен, вы получаете то же самое код сборки для 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). Это быстрее (2x так же быстро) с нулевыми итерациями, но тогда вы получаете только 12 бит точность. Не знаю, хватит ли тебе 12 бит.

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

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

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


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

во-первых, давайте посмотрим, почему корень Кармака работает:

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

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

e' = уклоном - E / 2 = 3/2 смещение-e / 2

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

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


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

e' = уклоном - 2 E = 3 смещение-2 e

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


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


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

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