Как улучшить квадратный корень с фиксированной точкой для небольших значений

Я использую библиотеку фиксированной точки Энтони Уильямса, описанную в статье доктора Добба "Оптимизация приложений, интенсивно использующих математические вычисления, с помощью арифметики с фиксированной точкой" для расчета расстояния между двумя географическими точками с использованием метод локсодромии.

Это работает достаточно хорошо, когда расстояния между точками значительны (более нескольких километров), но очень плохо на меньших расстояниях. В худшем случае, когда две точки равны или почти равны, результатом будет расстояние 194 метра, в то время как мне нужна точность не менее 1 метра на расстояниях> = 1 метр.

По сравнению с реализацией двойной точности с плавающей запятой я обнаружил проблему в функции fixed::sqrt(), которая плохо работает при малых значениях:

x       std::sqrt(x)    fixed::sqrt(x)  error
----------------------------------------------------
0       0               3.05176e-005    3.05176e-005
1e-005  0.00316228      0.00316334      1.06005e-006
2e-005  0.00447214      0.00447226      1.19752e-007
3e-005  0.00547723      0.0054779       6.72248e-007
4e-005  0.00632456      0.00632477      2.12746e-007
5e-005  0.00707107      0.0070715       4.27244e-007
6e-005  0.00774597      0.0077467       7.2978e-007
7e-005  0.0083666       0.00836658      1.54875e-008
8e-005  0.00894427      0.00894427      1.085e-009

Исправить результат для fixed::sqrt(0) тривиально, рассматривая его как частный случай, но это не решит проблему для небольших ненулевых расстояний, где ошибка начинается со 194 метров и сходится к нулю с увеличением расстояния. Мне, вероятно, нужно по крайней мере порядок увеличения точности до нуля.

Алгоритм fixed::sqrt() кратко объяснен на странице 4 статьи, указанной выше, но я изо всех сил пытаюсь следовать ему, не говоря уже о том, чтобы определить, можно ли его улучшить. Код функции воспроизведен ниже:

fixed fixed::sqrt() const
{
    unsigned const max_shift=62;
    uint64_t a_squared=1LL<<max_shift;
    unsigned b_shift=(max_shift+fixed_resolution_shift)/2;
    uint64_t a=1LL<<b_shift;

    uint64_t x=m_nVal;

    while(b_shift && a_squared>x)
    {
        a>>=1;
        a_squared>>=2;
        --b_shift;
    }

    uint64_t remainder=x-a_squared;
    --b_shift;

    while(remainder && b_shift)
    {
        uint64_t b_squared=1LL<<(2*b_shift-fixed_resolution_shift);
        int const two_a_b_shift=b_shift+1-fixed_resolution_shift;
        uint64_t two_a_b=(two_a_b_shift>0)?(a<<two_a_b_shift):(a>>-two_a_b_shift);

        while(b_shift && remainder<(b_squared+two_a_b))
        {
            b_squared>>=2;
            two_a_b>>=1;
            --b_shift;
        }
        uint64_t const delta=b_squared+two_a_b;
        if((2*remainder)>delta)
        {
            a+=(1LL<<b_shift);
            remainder-=delta;
            if(b_shift)
            {
                --b_shift;
            }
        }
    }
    return fixed(internal(),a);
}

Обратите внимание, что m_nVal — это внутреннее значение представления с фиксированной точкой, это int64_t, и в представлении используется Q36. формат 28 (fixed_resolution_shift = 28). Само представление имеет достаточную точность, по крайней мере, для 8 знаков после запятой, и поскольку часть экваториальной дуги подходит для расстояний около 0,14 метра, поэтому ограничение не связано с представлением с фиксированной точкой.

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

Вопрос. Можно ли повысить точность алгоритма fixed::sqrt() для небольших ненулевых значений, сохранив при этом его ограниченную и детерминированную сходимость?

Дополнительная информация. Тестовый код, использованный для создания приведенной выше таблицы:

#include <cmath>
#include <iostream>
#include "fixed.hpp"

int main()
{
    double error = 1.0 ;
    for( double x = 0.0; error > 1e-8; x += 1e-5 )
    {
        double fixed_root = sqrt(fixed(x)).as_double() ;
        double std_root = std::sqrt(x) ;
        error = std::fabs(fixed_root - std_root) ;
        std::cout << x << '\t' << std_root << '\t' << fixed_root << '\t' << error << std::endl ;
    }
}

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

fixed fixed::sqrt() const
{
    uint64_t a = 0 ;            // root accumulator
    uint64_t remHi = 0 ;        // high part of partial remainder
    uint64_t remLo = m_nVal ;   // low part of partial remainder
    uint64_t testDiv ;
    int count = 31 + (fixed_resolution_shift >> 1); // Loop counter
    do 
    {
        // get 2 bits of arg
        remHi = (remHi << 2) | (remLo >> 62); remLo <<= 2 ;

        // Get ready for the next bit in the root
        a <<= 1;   

        // Test radical
        testDiv = (a << 1) + 1;    
        if (remHi >= testDiv) 
        {
            remHi -= testDiv;
            a += 1;
        }

    } while (count-- != 0);

    return fixed(internal(),a);
}

Хотя это дает гораздо большую точность, нужного мне улучшения достичь не удастся. Сам по себе формат Q36.28 почти обеспечивает необходимую мне точность, но невозможно выполнить sqrt() без потери нескольких битов точности. Однако некоторое нестандартное мышление обеспечивает лучшее решение. Мое приложение проверяет расчетное расстояние на соответствие некоторому пределу расстояния. Оглядываясь назад, довольно очевидное решение состоит в том, чтобы сравнить квадрат расстояния с квадратом предела!


person Clifford    schedule 04.01.2012    source источник
comment
Клиффорд - URL-адрес статьи был скрыт (с запросом входа в систему UBM Techweb, вы, вероятно, вошли в систему и не столкнулись с этим). Я пытался найти статью в другом месте, но не нашел - кеш Google показался лучшей альтернативой. Спасибо за ссылку на статью в первую очередь.   -  person Dan    schedule 04.01.2012
comment
@ Дэн, я перешел по исходной ссылке, и у меня не было проблем. Я никогда не использовал UBM и не думаю, что вошел в систему DDJ, поэтому не знаю, в чем проблема.   -  person Mark Ransom    schedule 04.01.2012
comment
Я использую код уже пару лет; Я загрузил библиотеку, вероятно, до требования входа в систему. Получите его на собственном веб-сайте Энтони   -  person Clifford    schedule 04.01.2012
comment
@MarkRansom - Хорошо, мне нужно было разобраться в этом ... У меня были файлы cookie из drdobbs/ubm/ddj, как только я их стер, они позволили мне войти, не заставляя меня входить в систему. Хорошо, дайте своим зарегистрированным пользователям больше препятствий.   -  person Dan    schedule 04.01.2012
comment
Как именно вы получили числа от fixed::sqrt(), показанные в таблице? Какой компилятор + ОС вы использовали? Я не получаю одинаковые числа ни для чего, кроме квадратного корня из 0. Ни с gcc (DJGPP/DOS), ни с Open Watcom (Windows). Все мои результаты отличаются примерно от 10^-5 до 10^-6 для x из таблицы, а не на 10^-7 или 10^-9. Вы использовали более 28 дробных бит при заполнении таблицы? Как вы конвертировали в/из fixed и каков размер вашего типа с плавающей запятой (кстати, это было double или long double)?   -  person Alexey Frunze    schedule 05.01.2012
comment
@Alex: Тест проводился с использованием VC++, но идентичные результаты были получены с использованием ARM RealView. Преобразование было выполнено с помощью функции-члена fixed::to_double(). Я добавлю тестовый код к вопросу, как только смогу - в настоящее время он мне недоступен. Все типы в тесте либо фиксированные, либо двойные. Ошибку вычислил fabs( std::sqrt(x) - sqrt(fixed(x)))   -  person Clifford    schedule 05.01.2012
comment
@Alex: исправление... преобразование было с fixed::as_double(). as_double() выполняет x / (double)1<<fixed_resolution_shift   -  person Clifford    schedule 05.01.2012


Ответы (4)


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

Я рассматриваю входное число как находящееся в Q64 для начала, что совпадает со сдвигом на 28, а затем смещением назад на 14 после этого (sqrt делит его пополам). Однако, если вы просто сделаете это, то точность будет ограничена 1/2 ^ 14 = 6,1035e-5, потому что последние 14 бит будут равны 0. Чтобы исправить это, я затем правильно сдвигаю a и remainder и продолжаю заполнять цифры. Я снова делаю петлю. Код можно сделать более эффективным и чистым, но я оставлю это кому-то другому. Точность, показанная ниже, настолько же хороша, насколько вы можете получить с помощью Q36.28. Если вы сравните sqrt с фиксированной запятой с sqrt с плавающей запятой входного числа после того, как оно было усечено фиксированной точкой (преобразование его в фиксированную точку и обратно), то ошибки составляют около 2e-9 (я не делал этого в приведенный ниже код, но для этого требуется одна строка изменения). Это соответствует наилучшей точности для Q36.28, которая составляет 1/2^28 = 3,7529e-9.

Кстати, одна большая ошибка в исходном коде заключается в том, что термин, где m = 0, никогда не учитывается, поэтому этот бит никогда не может быть установлен. В любом случае, вот код. Наслаждаться!

#include <iostream>
#include <cmath>

typedef unsigned long uint64_t;

uint64_t sqrt(uint64_t in_val)
{
    const uint64_t fixed_resolution_shift = 28;
    const unsigned max_shift=62;
    uint64_t a_squared=1ULL<<max_shift;
    unsigned b_shift=(max_shift>>1) + 1;
    uint64_t a=1ULL<<(b_shift - 1);

    uint64_t x=in_val;

    while(b_shift && a_squared>x)
    {
        a>>=1;
        a_squared>>=2;
        --b_shift;
    }

    uint64_t remainder=x-a_squared;
    --b_shift;

    while(remainder && b_shift)
    {
        uint64_t b_squared=1ULL<<(2*(b_shift - 1));
        uint64_t two_a_b=(a<<b_shift);

        while(b_shift && remainder<(b_squared+two_a_b))
        {
            b_squared>>=2;
            two_a_b>>=1;
            --b_shift;
        }
        uint64_t const delta=b_squared+two_a_b;
        if((remainder)>=delta && b_shift)
        {
            a+=(1ULL<<(b_shift - 1));
            remainder-=delta;
            --b_shift;
        }
    }
    a <<= (fixed_resolution_shift/2);
    b_shift = (fixed_resolution_shift/2) + 1;
    remainder <<= (fixed_resolution_shift);

    while(remainder && b_shift)
    {
        uint64_t b_squared=1ULL<<(2*(b_shift - 1));
        uint64_t two_a_b=(a<<b_shift);

        while(b_shift && remainder<(b_squared+two_a_b))
        {
            b_squared>>=2;
            two_a_b>>=1;
            --b_shift;
        }
        uint64_t const delta=b_squared+two_a_b;
        if((remainder)>=delta && b_shift)
        {
            a+=(1ULL<<(b_shift - 1));
            remainder-=delta;
            --b_shift;
        }
    }

    return a;
}

double fixed2float(uint64_t x)
{
    return static_cast<double>(x) * pow(2.0, -28.0);
}

uint64_t float2fixed(double f)
{
    return static_cast<uint64_t>(f * pow(2, 28.0));
}

void finderror(double num)
{
    double root1 = fixed2float(sqrt(float2fixed(num)));
    double root2 = pow(num, 0.5);
    std::cout << "input: " << num << ", fixed sqrt: " << root1 << " " << ", float sqrt: " << root2 << ", finderror: " << root2 - root1 << std::endl;
}

main()
{
    finderror(0);
    finderror(1e-5);
    finderror(2e-5);
    finderror(3e-5);
    finderror(4e-5);
    finderror(5e-5);
    finderror(pow(2.0,1));
    finderror(1ULL<<35);
}

с выводом программы

input: 0, fixed sqrt: 0 , float sqrt: 0, finderror: 0
input: 1e-05, fixed sqrt: 0.00316207 , float sqrt: 0.00316228, finderror: 2.10277e-07
input: 2e-05, fixed sqrt: 0.00447184 , float sqrt: 0.00447214, finderror: 2.97481e-07
input: 3e-05, fixed sqrt: 0.0054772 , float sqrt: 0.00547723, finderror: 2.43815e-08
input: 4e-05, fixed sqrt: 0.00632443 , float sqrt: 0.00632456, finderror: 1.26255e-07
input: 5e-05, fixed sqrt: 0.00707086 , float sqrt: 0.00707107, finderror: 2.06055e-07
input: 2, fixed sqrt: 1.41421 , float sqrt: 1.41421, finderror: 1.85149e-09
input: 3.43597e+10, fixed sqrt: 185364 , float sqrt: 185364, finderror: 2.24099e-09
person Justin Peel    schedule 05.01.2012
comment
Это именно то, о чем я просил, и более или менее простая замена существующего тела кода. К сожалению, моя оценка требуемой точности была неверной, и даже с таким огромным улучшением она недостаточна. Это повышает точность там, где sqrt() используется в другом месте, поэтому я, скорее всего, оставлю его. Я собираюсь рассмотреть это в качестве должной осмотрительности, но если, как вы говорите, это ограничение производительности, в этом случае мне придется использовать std::sqrt() и плавающую точку. - person Clifford; 05.01.2012
comment
Я сравнил результаты этого с алгоритмом в Забытое искусство арифметики с фиксированной точкой, и оно дает идентичные результаты и, вероятно, является более эффективной/чистой версией, о которой вы говорили. Вы, по крайней мере, заставили меня осознать пределы того, чего можно достичь с помощью Q36.28. Спасибо. - person Clifford; 05.01.2012

Учитывая, что sqrt(ab) = sqrt(a)sqrt(b), не можете ли вы просто отловить случай, когда ваше число мало, и сдвинуть его вверх на заданное количество бит, вычислить корень и сдвинуть его обратно на половину количества бит, чтобы получить результат?

I.e.

 sqrt(n) = sqrt(n.2^k)/sqrt(2^k)
         = sqrt(n.2^k).2^(-k/2)

Например. Выберите k = 28 для любого n меньше 2 ^ 8.

person Keith    schedule 04.01.2012
comment
Очень умное и эффективное решение. - person Jim Clay; 04.01.2012

Я не знаю, как вы получаете числа от fixed::sqrt(), показанные в таблице.

Вот что я делаю:

#include <stdio.h>
#include <math.h>

#define __int64 long long // gcc doesn't know __int64
typedef __int64 fixed;

#define FRACT 28

#define DBL2FIX(x) ((fixed)((double)(x) * (1LL << FRACT)))
#define FIX2DBL(x) ((double)(x) / (1LL << FRACT))

// De-++-ified code from
// http://www.justsoftwaresolutions.co.uk/news/optimizing-applications-with-fixed-point-arithmetic.html
fixed sqrtfix0(fixed num)
{
    static unsigned const fixed_resolution_shift=FRACT;

    unsigned const max_shift=62;
    unsigned __int64 a_squared=1LL<<max_shift;
    unsigned b_shift=(max_shift+fixed_resolution_shift)/2;
    unsigned __int64 a=1LL<<b_shift;

    unsigned __int64 x=num;

    unsigned __int64 remainder;

    while(b_shift && a_squared>x)
    {
        a>>=1;
        a_squared>>=2;
        --b_shift;
    }

    remainder=x-a_squared;
    --b_shift;

    while(remainder && b_shift)
    {
        unsigned __int64 b_squared=1LL<<(2*b_shift-fixed_resolution_shift);
        int const two_a_b_shift=b_shift+1-fixed_resolution_shift;
        unsigned __int64 two_a_b=(two_a_b_shift>0)?(a<<two_a_b_shift):(a>>-two_a_b_shift);
        unsigned __int64 delta;

        while(b_shift && remainder<(b_squared+two_a_b))
        {
            b_squared>>=2;
            two_a_b>>=1;
            --b_shift;
        }
        delta=b_squared+two_a_b;
        if((2*remainder)>delta)
        {
            a+=(1LL<<b_shift);
            remainder-=delta;
            if(b_shift)
            {
                --b_shift;
            }
        }
    }
    return (fixed)a;
}

// Adapted code from
// http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation
fixed sqrtfix1(fixed num)
{
    fixed res = 0;
    fixed bit = (fixed)1 << 62; // The second-to-top bit is set
    int s = 0;

    // Scale num up to get more significant digits

    while (num && num < bit)
    {
        num <<= 1;
        s++;
    }

    if (s & 1)
    {
        num >>= 1;
        s--;
    }

    s = 14 - (s >> 1);

    while (bit != 0)
    {
        if (num >= res + bit)
        {
            num -= res + bit;
            res = (res >> 1) + bit;
        }
        else
        {
            res >>= 1;
        }

        bit >>= 2;
    }

    if (s >= 0) res <<= s;
    else res >>= -s;

    return res;
}

int main(void)
{
    double testData[] =
    {
        0,
        1e-005,
        2e-005,
        3e-005,
        4e-005,
        5e-005,
        6e-005,
        7e-005,
        8e-005,
    };
    int i;

    for (i = 0; i < sizeof(testData) / sizeof(testData[0]); i++)
    {
        double x = testData[i];
        fixed xf = DBL2FIX(x);

        fixed sqf0 = sqrtfix0(xf);
        fixed sqf1 = sqrtfix1(xf);

        double sq0 = FIX2DBL(sqf0);
        double sq1 = FIX2DBL(sqf1);

        printf("%10.8f:  "
               "sqrtfix0()=%10.8f / err=%e  "
               "sqrt()=%10.8f  "
               "sqrtfix1()=%10.8f / err=%e\n",
               x,
               sq0, fabs(sq0 - sqrt(x)),
               sqrt(x),
               sq1, fabs(sq1 - sqrt(x)));
    }

    printf("sizeof(double)=%d\n", (int)sizeof(double));

    return 0;
}

И вот что я получаю (с gcc и Open Watcom):

0.00000000:  sqrtfix0()=0.00003052 / err=3.051758e-05  sqrt()=0.00000000  sqrtfix1()=0.00000000 / err=0.000000e+00
0.00001000:  sqrtfix0()=0.00311279 / err=4.948469e-05  sqrt()=0.00316228  sqrtfix1()=0.00316207 / err=2.102766e-07
0.00002000:  sqrtfix0()=0.00445557 / err=1.656955e-05  sqrt()=0.00447214  sqrtfix1()=0.00447184 / err=2.974807e-07
0.00003000:  sqrtfix0()=0.00543213 / err=4.509667e-05  sqrt()=0.00547723  sqrtfix1()=0.00547720 / err=2.438148e-08
0.00004000:  sqrtfix0()=0.00628662 / err=3.793423e-05  sqrt()=0.00632456  sqrtfix1()=0.00632443 / err=1.262553e-07
0.00005000:  sqrtfix0()=0.00701904 / err=5.202484e-05  sqrt()=0.00707107  sqrtfix1()=0.00707086 / err=2.060551e-07
0.00006000:  sqrtfix0()=0.00772095 / err=2.501943e-05  sqrt()=0.00774597  sqrtfix1()=0.00774593 / err=3.390476e-08
0.00007000:  sqrtfix0()=0.00836182 / err=4.783859e-06  sqrt()=0.00836660  sqrtfix1()=0.00836649 / err=1.086198e-07
0.00008000:  sqrtfix0()=0.00894165 / err=2.621519e-06  sqrt()=0.00894427  sqrtfix1()=0.00894409 / err=1.777289e-07
sizeof(double)=8

ИЗМЕНИТЬ:

Я упустил тот факт, что приведенный выше sqrtfix1() не будет работать с большими аргументами. Это можно исправить, добавив к аргументу 28 нулей и, по существу, вычислив точный целочисленный квадратный корень из него. Это происходит за счет выполнения внутренних вычислений в 128-битной арифметике, но это довольно просто:

fixed sqrtfix2(fixed num)
{
    unsigned __int64 numl, numh;
    unsigned __int64 resl = 0, resh = 0;
    unsigned __int64 bitl = 0, bith = (unsigned __int64)1 << 26;

    numl = num << 28;
    numh = num >> (64 - 28);

    while (bitl | bith)
    {
        unsigned __int64 tmpl = resl + bitl;
        unsigned __int64 tmph = resh + bith + (tmpl < resl);

        tmph = numh - tmph - (numl < tmpl);
        tmpl = numl - tmpl;

        if (tmph & 0x8000000000000000ULL)
        {
            resl >>= 1;
            if (resh & 1) resl |= 0x8000000000000000ULL;
            resh >>= 1;
        }
        else
        {
            numl = tmpl;
            numh = tmph;

            resl >>= 1;
            if (resh & 1) resl |= 0x8000000000000000ULL;
            resh >>= 1;

            resh += bith + (resl + bitl < resl);
            resl += bitl;
        }

        bitl >>= 2;
        if (bith & 1) bitl |= 0x4000000000000000ULL;
        if (bith & 2) bitl |= 0x8000000000000000ULL;
        bith >>= 2;
    }

    return resl;
}

И это дает почти те же результаты (немного лучше для 3.43597e+10), чем этот ответ:

0.00000000:  sqrtfix0()=0.00003052 / err=3.051758e-05  sqrt()=0.00000000  sqrtfix2()=0.00000000 / err=0.000000e+00
0.00001000:  sqrtfix0()=0.00311279 / err=4.948469e-05  sqrt()=0.00316228  sqrtfix2()=0.00316207 / err=2.102766e-07
0.00002000:  sqrtfix0()=0.00445557 / err=1.656955e-05  sqrt()=0.00447214  sqrtfix2()=0.00447184 / err=2.974807e-07
0.00003000:  sqrtfix0()=0.00543213 / err=4.509667e-05  sqrt()=0.00547723  sqrtfix2()=0.00547720 / err=2.438148e-08
0.00004000:  sqrtfix0()=0.00628662 / err=3.793423e-05  sqrt()=0.00632456  sqrtfix2()=0.00632443 / err=1.262553e-07
0.00005000:  sqrtfix0()=0.00701904 / err=5.202484e-05  sqrt()=0.00707107  sqrtfix2()=0.00707086 / err=2.060551e-07
0.00006000:  sqrtfix0()=0.00772095 / err=2.501943e-05  sqrt()=0.00774597  sqrtfix2()=0.00774593 / err=3.390476e-08
0.00007000:  sqrtfix0()=0.00836182 / err=4.783859e-06  sqrt()=0.00836660  sqrtfix2()=0.00836649 / err=1.086198e-07
0.00008000:  sqrtfix0()=0.00894165 / err=2.621519e-06  sqrt()=0.00894427  sqrtfix2()=0.00894409 / err=1.777289e-07
2.00000000:  sqrtfix0()=1.41419983 / err=1.373327e-05  sqrt()=1.41421356  sqrtfix2()=1.41421356 / err=1.851493e-09
34359700000.00000000:  sqrtfix0()=185363.69654846 / err=5.097361e-06  sqrt()=185363.69655356  sqrtfix2()=185363.69655356 / err=1
.164153e-09
person Alexey Frunze    schedule 04.01.2012

Много-много лет назад я работал над демонстрационной программой для небольшого компьютера, который построила наша команда. У компьютера была встроенная инструкция извлечения квадратного корня, и мы создали простую программу, чтобы продемонстрировать, как компьютер выполняет 16-разрядное сложение/вычитание/умножение/деление/извлечение квадратного корня на TTY. Увы, оказалось, что в инструкции извлечения квадратного корня серьезная ошибка, но мы обещали продемонстрировать функцию. Итак, мы создали массив квадратов значений 1-255, а затем использовали простой поиск, чтобы сопоставить введенное значение с одним из значений массива. Индекс был квадратным корнем.

person Hot Licks    schedule 04.01.2012
comment
К сожалению, мне нужна более высокая точность в более широком диапазоне, что было бы возможно при поиске. - person Clifford; 04.01.2012