2012-01-04 14 views
11

Utilizzo la libreria a punti fissi di Anthony Williams descritta nell'articolo del dott. Dobb "Optimizing Math-Intensive Applications with Fixed-Point Arithmetic" per calcolare la distanza tra due punti geografici utilizzando Rhumb Line method.Come migliorare la radice quadrata a punti fissi per valori piccoli

Questo funziona abbastanza bene quando la distanza tra i punti è significativa (superiore a pochi chilometri), ma è molto scarsa a distanze minori. Nel peggiore dei casi, quando i due punti sono uguali o quasi uguali, il risultato è una distanza di 194 metri, mentre ho bisogno di una precisione di almeno 1 metro a distanze> = 1 metro.

In confronto con una doppia precisione implementazione in virgola mobile, ho individuato il problema alla funzione fixed::sqrt(), che funziona male a piccoli valori:

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 

Correzione risultato fixed::sqrt(0) è banale ravvisandovi un caso speciale, ma che non risolverà il problema per piccole distanze diverse da zero, dove l'errore inizia a 194 metri e converge verso lo zero con l'aumentare della distanza. Probabilmente ho bisogno di almeno un ordine di miglioramento della magnitudine in precisione verso lo zero.

L'algoritmo fixed::sqrt() è brevemente spiegato a pagina 4 dell'articolo correlato sopra, ma sto lottando per seguirlo e tanto meno determinare se sia possibile migliorarlo. Il codice per la funzione è riportato di seguito:

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

noti che m_nVal è il valore rappresentazione punto fisso interno, è un int64_t e la rappresentazione utilizza Q36.28 formato (fixed_resolution_shift = 28). La rappresentazione stessa ha una precisione sufficiente per almeno 8 posizioni decimali e una frazione dell'arco equatoriale è buona per distanze di circa 0,14 metri, quindi la limitazione non è la rappresentazione in virgola fissa.

L'uso del metodo della lossodromia è un consiglio per il corpo standard per questa applicazione, pertanto non può essere modificato e, in ogni caso, è probabile che sia necessaria una funzione radice quadrata più accurata altrove nell'applicazione o nelle applicazioni future.

Domanda: È possibile migliorare la precisione dell'algoritmo fixed::sqrt() per piccoli valori diversi da zero pur mantenendo la convergenza limitata e deterministica?

Informazioni aggiuntive Il codice di test utilizzato per generare la tabella di cui sopra:

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

Conclusione Alla luce della soluzione ed analisi di Justin Peel, e confronto con l'algoritmo in "The Neglected Art of Fixed Point Arithmetic", ho adattato quest'ultimo come segue:

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

Mentre questo dà maggiore precisione, il miglioramento di cui avevo bisogno non è stato raggiunto. Il formato Q36.28 fornisce solo la precisione di cui ho bisogno, ma non è possibile eseguire un sqrt() senza perdita di alcuni bit di precisione. Tuttavia alcuni pensieri laterali offrono una soluzione migliore. La mia applicazione verifica la distanza calcolata rispetto ad un limite di distanza. La soluzione piuttosto ovvia col senno di poi è testare il quadrato della distanza contro il quadrato del limite!

+0

Clifford - l'URL per l'articolo è stato hosed (per chiedere un accesso UBM TechWeb, si erano probabilmente effettuato l'accesso e non ha esperienza questo). Ho cercato di trovare l'articolo altrove ma non è stato così: la cache di Google sembrava essere l'alternativa migliore. Grazie per aver fatto riferimento all'articolo in primo luogo. – Dan

+0

@Dan, sono andato al link originale e non ho avuto problemi. Non ho mai usato UBM e non penso di aver effettuato l'accesso a DDJ, quindi non so perché c'è un problema. –

+0

Ho usato il codice per un paio di anni; Ho scaricato la libreria probabilmente prima del requisito di accesso. Scaricalo dal [sito web di Anthony] (http://www.justsoftwaresolutions.co.uk/news/optimizing-applications-with-fixed-point-arithmetic.html) – Clifford

risposta

4

L'implementazione originale ha ovviamente alcuni problemi. Mi sono sentito frustrato dal tentativo di risolverli tutti con il modo in cui il codice è attualmente in esecuzione e ho finito per affrontarlo con un approccio diverso. Probabilmente ora potrei sistemare l'originale, ma comunque mi piace di più.

Trovo il numero di input come in Q64 per iniziare, che è lo stesso di uno spostamento di 28 e quindi di tornare indietro di 14 in seguito (lo sqrt dimezza). Tuttavia, se lo fai, la precisione è limitata a 1/2^14 = 6.1035e-5 perché gli ultimi 14 bit saranno pari a 0. Per ovviare a questo, allora cambio a e remainder correttamente e per continuare a riempire le cifre Faccio di nuovo il ciclo. Il codice può essere reso più efficiente e più pulito, ma lo lascerò a qualcun altro. La precisione mostrata di seguito è praticamente la migliore che puoi ottenere con Q36.28. Se si confronta il sqrt a virgola fissa con il sqrt a virgola mobile del numero di input dopo che è stato troncato dal punto fisso (convertendolo in punto fisso e viceversa), gli errori sono intorno a 2e-9 (non l'ho fatto in il codice qui sotto, ma richiede una linea di cambiamento). Questo è perfettamente in linea con la migliore accuratezza per Q36.28 che è 1/2^28 = 3.7529e-9.

A proposito, un grosso errore nel codice originale è che il termine in cui m = 0 non viene mai considerato in modo che il bit non possa mai essere impostato. Ad ogni modo, ecco il codice. Godere!

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

con l'uscita del programma di essere

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 
+0

Questo è esattamente ciò che ho chiesto, e più o meno una sostituzione drop-in per il corpo del codice esistente. Sfortunatamente la mia stima della precisione richiesta non era corretta e anche con questo vasto miglioramento non è sufficiente. Ciò migliora l'accuratezza in cui sqrt() viene utilizzato altrove, quindi è probabile che lo conservi. Ho intenzione di considerarlo ulteriormente come dovuta diligenza, ma se come dici tu questa è la limitazione della performance, in questa istanza dovrò usare lo std :: sqrt() e il punto mobile. – Clifford

+0

Ho confrontato i risultati di questo con l'algoritmo in * ["The Neglected Art of Fixed Point Arithmetic"] (http://jet.ro/2006/08/07/neglected-art-of-fixed-point-arithmetic/) *, e produce risultati identici ed è probabilmente la versione più efficiente/pulita a cui ti riferivi. Almeno mi hai fatto capire i limiti di ciò che può essere raggiunto con Q36.28. Grazie. – Clifford

0

Molti anni fa ho lavorato a un programma dimostrativo per un piccolo computer che il nostro negozio aveva costruito. Il computer disponeva di un'istruzione di radice quadrata incorporata e abbiamo creato un semplice programma per dimostrare che il computer eseguiva/sottrarre/sottrarre/moltiplicare/dividere/radice quadrata su un TTY. Purtroppo, si è scoperto che c'era un bug grave nell'istruzione della radice quadrata, ma avevamo promesso di provare la funzione. Quindi abbiamo creato una matrice dei quadrati dei valori 1-255, quindi abbiamo utilizzato una semplice ricerca per abbinare il valore digitato in uno dei valori dell'array. L'indice era la radice quadrata.

+0

Purtroppo ho bisogno di una precisione migliore su una gamma più ampia che sarebbe fattibile con una ricerca. – Clifford

11

Dato che sqrt(ab) = sqrt(a)sqrt(b), allora non puoi semplicemente intrappolare il caso in cui il tuo numero è piccolo e spostarlo di un numero dato di bit, calcolare la radice e spostare indietro di metà del numero di bit per ottenere il risultato?

I.e.

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

E.g. Scegli k = 28 per ogni n in meno di 2^8.

+0

Soluzione molto intelligente ed efficiente. –

1

Non sono sicuro di come vengano visualizzati i numeri da fixed::sqrt() nella tabella.

Ecco quello che faccio:

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

Ed ecco quello che ottengo (con gcc e 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 

EDIT:

ho perso il fatto che il precedente sqrtfix1() non funzionerà bene con argomenti di grandi dimensioni. Può essere risolto aggiungendo 28 zeri all'argomento e calcolando essenzialmente la radice quadrata intera esatta di quello. Ciò va a scapito di fare calcoli interni a 128 bit aritmetica, ma è piuttosto semplice:

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

E dà praticamente gli stessi risultati (leggermente migliori per 3.43597e + 10) rispetto this answer:

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 
Problemi correlati