Оптимизация sqrt ( n) - sqrt(n-1)

вот функция, которую я вызываю много раз в секунду:

static inline double calculate_scale(double n) { //n may be int or double
    return sqrt(n) - sqrt(n-1);
}

вызывается в цикле, как:

for(double i = 0; i < x; i++) {
    double scale = calculate_scale(i);
    ...
}

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

параметр n: старт с 1 вверх, практически не ограниченный, но главным образом использованный с небольшими номерами в границах 1-10. Это целое число (целое число), но оно может быть как int или double в зависимости от того, что работает лучше.

3 ответов


вы можете попытаться заменить его следующим приближением

sqrt(n) - sqrt(n-1) == 
(sqrt(n) - sqrt(n-1)) * (sqrt(n) + sqrt(n-1)) / (sqrt(n) + sqrt(n-1)) ==
(n - (n + 1)) / (sqrt(n) + sqrt(n-1)) ==
1 / (sqrt(n) + sqrt(n-1))

для достаточно больших n, последнее уравнение довольно близко к 1 / (2 * sqrt(n)). Так что вам нужно только позвонить sqrt раз. Также стоит отметить, что даже без аппроксимации последнее выражение более численно устойчиво с точки зрения относительной ошибки для большего n.


прежде всего, спасибо за все предложения. Я провел некоторые исследования и нашел интересные реализации и факты.

1. В цикле или с использованием предварительно вычисленной таблицы

(спасибо @Ulysse BN) Вы можете оптимизировать цикл, просто сохранив previous sqrt(n) значение. В следующем примере показана оптимизация, используемая для настройки предварительно вычисленной таблицы.

    /**
     * Init variables
     *      i       counter
     *      x       number of cycles (size of table)
     *      sqrtI1  previous square root = sqrt(i-1)
     *      ptr     Pointer for next value
     */
    double i, x = sizeof(precomputed_table) / sizeof(double);
    double sqrtI1 = 0;

    double* ptr = (double*) precomputed_table;

    /**
     * Optimized calculation
     * In short:
     *      scale = sqrt(i) - sqrt(i-1)
     */
    for(i = 1; i <= x; i++) {
        double sqrtI = sqrt(i);
        double scale = sqrtI - sqrtI1; 
        *ptr++ = scale;
        sqrtI1 = sqrtI;
    }

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

static inline double calculate_scale(int n) {
    return precomputed_table[n-1];
}


2. Аппроксимация для больших чисел с использованием обратного квадратного корня

требуется обратная (взаимная) функция квадратного корня rsqrt

этот метод имеет наиболее точные результаты, с большим количеством. При малых числах возникают ошибки:

1    2     3      10       100     1000
0.29 0.006 0.0016 0.000056 1.58e-7 4.95e-10

вот код JS, который я использовал для расчета результатов выше:

function sqrt(x) { return Math.sqrt(x); } function d(x) { return (sqrt(x)-sqrt(x-1))-(0.5/sqrt(x-0.5));} console.log(d(1), d(2), d(3), d(10), d(100), d(1000));

вы можете также увидеть точность сравненную с версией 2-sqrt внутри одиночный график:https://www.google.com/search?q=(sqrt (x) - sqrt (x-1)) - (0.5%2Fsqrt(x-0.5))

использование:

static inline double calculate_scale(double n) {
    //Same as: 0.5 / sqrt(n-0.5)
    //but lot faster
    return 0.5 * rsqrt(n-0.5);
}

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

static inline float calculate_scale(float n) {
    return 0.5 * Q_rsqrt(n-0.5);
}

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

подробнее о реализации см. https://en.wikipedia.org/wiki/Fast_inverse_square_root и http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf . Не рекомендуется использовать современные процессоры с аппаратным взаимным квадратным корнем.

не всегда решение: 0.5 / sqrt (n-0.5)

обратите внимание, что на некоторых процессорах (например. ARM Cortex A9, Процессор Intel Сердечником2) дивизия берет почти же время как оборудование квадратный корень, поэтому лучше всего использовать original функция с 2 квадратными корнями sqrt(n) - sqrt(n-1) или взаимный квадратный корень с умножением вместо 0.5 * rsqrt(n-0.5) если существует.


3. Использование предварительно вычисленной таблицы с резервным

этот метод является хорошим компромиссом между первыми 2 решениями. Он имеет и хорошую точность и представление.

static inline double calculate_scale(double n) {
    if(n <= sizeof_precomputed_table) {
        int nIndex = (int) n;
        return precomputed_table[nIndex-1];
    }
    //Multiply + Inverse Square root
    return 0.5 * rsqrt(n-0.5);
    //OR
    return sqrt(n) - sqrt(n-1);
}

в моем случае мне нужны действительно точные цифры, поэтому мой предварительно вычисленный размер таблицы-2048.

любая обратная связь приветствуется.


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

код будет выглядеть примерно так:

static inline double calculate_scale(double n) { //n may be int or double
    if (n <= 10.0 && n == floor(n)) {
        return precomputed[(int) n]
    }
    return sqrt(n) - sqrt(n-1);
}