web-dev-qa-db-fra.com

Recherche d'un algorithme efficace de racine carrée entière pour ARM Thumb2

Je recherche un algorithme rapide et entier uniquement pour trouver la racine carrée (partie entière de celui-ci) d'un entier non signé. Le code doit avoir d'excellentes performances sur les processeurs ARM Thumb 2). Il peut s'agir du langage d'assemblage ou du code C.

Tous les indices sont les bienvenus.

42
Ber

Integer Square Roots par Jack W. Crenshaw pourrait être utile comme autre référence.

C Snippets Archive a également une implémentation de racine carrée entière . Celui-ci va au-delà du résultat entier et calcule des bits fractionnaires supplémentaires (virgule fixe) de la réponse. (Mise à jour: malheureusement, l'archive des extraits de code C est désormais disparue. Le lien pointe vers l'archive Web de la page.) Voici le code de l'archive des extraits de code C:

#define BITSPERLONG 32
#define TOP2BITS(x) ((x & (3L << (BITSPERLONG-2))) >> (BITSPERLONG-2))

struct int_sqrt {
    unsigned sqrt, frac;
};

/* usqrt:
    ENTRY x: unsigned long
    EXIT  returns floor(sqrt(x) * pow(2, BITSPERLONG/2))

    Since the square root never uses more than half the bits
    of the input, we use the other half of the bits to contain
    extra bits of precision after the binary point.

    EXAMPLE
        suppose BITSPERLONG = 32
        then    usqrt(144) = 786432 = 12 * 65536
                usqrt(32) = 370727 = 5.66 * 65536

    NOTES
        (1) change BITSPERLONG to BITSPERLONG/2 if you do not want
            the answer scaled.  Indeed, if you want n bits of
            precision after the binary point, use BITSPERLONG/2+n.
            The code assumes that BITSPERLONG is even.
        (2) This is really better off being written in Assembly.
            The line marked below is really a "arithmetic shift left"
            on the double-long value with r in the upper half
            and x in the lower half.  This operation is typically
            expressible in only one or two Assembly instructions.
        (3) Unrolling this loop is probably not a bad idea.

    ALGORITHM
        The calculations are the base-two analogue of the square
        root algorithm we all learned in grammar school.  Since we're
        in base 2, there is only one nontrivial trial multiplier.

        Notice that absolutely no multiplications or divisions are performed.
        This means it'll be fast on a wide range of processors.
*/

void usqrt(unsigned long x, struct int_sqrt *q)
{
      unsigned long a = 0L;                   /* accumulator      */
      unsigned long r = 0L;                   /* remainder        */
      unsigned long e = 0L;                   /* trial product    */

      int i;

      for (i = 0; i < BITSPERLONG; i++)   /* NOTE 1 */
      {
            r = (r << 2) + TOP2BITS(x); x <<= 2; /* NOTE 2 */
            a <<= 1;
            e = (a << 1) + 1;
            if (r >= e)
            {
                  r -= e;
                  a++;
            }
      }
      memcpy(q, &a, sizeof(long));
}

J'ai choisi le code suivant. C'est essentiellement du article Wikipedia sur les méthodes de calcul à racine carrée . Mais il a été modifié pour utiliser stdint.h les types uint32_t etc. À strictement parler, le type de retour pourrait être changé en uint16_t.

/**
 * \brief    Fast Square root algorithm
 *
 * Fractional parts of the answer are discarded. That is:
 *      - SquareRoot(3) --> 1
 *      - SquareRoot(4) --> 2
 *      - SquareRoot(5) --> 2
 *      - SquareRoot(8) --> 2
 *      - SquareRoot(9) --> 3
 *
 * \param[in] a_nInput - unsigned integer for which to find the square root
 *
 * \return Integer square root of the input value.
 */
uint32_t SquareRoot(uint32_t a_nInput)
{
    uint32_t op  = a_nInput;
    uint32_t res = 0;
    uint32_t one = 1uL << 30; // The second-to-top bit is set: use 1u << 14 for uint16_t type; use 1uL<<30 for uint32_t type


    // "one" starts at the highest power of four <= than the argument.
    while (one > op)
    {
        one >>= 2;
    }

    while (one != 0)
    {
        if (op >= res + one)
        {
            op = op - (res + one);
            res = res +  2 * one;
        }
        res >>= 1;
        one >>= 2;
    }
    return res;
}

La bonne chose, j'ai découvert, c'est qu'une modification assez simple peut retourner la réponse "arrondie". J'ai trouvé cela utile dans une certaine application pour une plus grande précision. Notez que dans ce cas, le type de retour doit être uint32_t car la racine carrée arrondie de 232- 1 est 216.

/**
 * \brief    Fast Square root algorithm, with rounding
 *
 * This does arithmetic rounding of the result. That is, if the real answer
 * would have a fractional part of 0.5 or greater, the result is rounded up to
 * the next integer.
 *      - SquareRootRounded(2) --> 1
 *      - SquareRootRounded(3) --> 2
 *      - SquareRootRounded(4) --> 2
 *      - SquareRootRounded(6) --> 2
 *      - SquareRootRounded(7) --> 3
 *      - SquareRootRounded(8) --> 3
 *      - SquareRootRounded(9) --> 3
 *
 * \param[in] a_nInput - unsigned integer for which to find the square root
 *
 * \return Integer square root of the input value.
 */
uint32_t SquareRootRounded(uint32_t a_nInput)
{
    uint32_t op  = a_nInput;
    uint32_t res = 0;
    uint32_t one = 1uL << 30; // The second-to-top bit is set: use 1u << 14 for uint16_t type; use 1uL<<30 for uint32_t type


    // "one" starts at the highest power of four <= than the argument.
    while (one > op)
    {
        one >>= 2;
    }

    while (one != 0)
    {
        if (op >= res + one)
        {
            op = op - (res + one);
            res = res +  2 * one;
        }
        res >>= 1;
        one >>= 2;
    }

    /* Do arithmetic rounding to nearest integer */
    if (op > res)
    {
        res++;
    }

    return res;
}
32
Craig McQueen

Si une précision exacte n'est pas requise, j'ai une approximation rapide pour vous, qui utilise 260 octets de RAM (vous pouvez diviser cela par deux, mais pas).

int ftbl[33]={0,1,1,2,2,4,5,8,11,16,22,32,45,64,90,128,181,256,362,512,724,1024,1448,2048,2896,4096,5792,8192,11585,16384,23170,32768,46340};
int ftbl2[32]={ 32768,33276,33776,34269,34755,35235,35708,36174,36635,37090,37540,37984,38423,38858,39287,39712,40132,40548,40960,41367,41771,42170,42566,42959,43347,43733,44115,44493,44869,45241,45611,45977};

int fisqrt(int val)
{
    int cnt=0;
    int t=val;
    while (t) {cnt++;t>>=1;}
    if (6>=cnt)    t=(val<<(6-cnt));
    else           t=(val>>(cnt-6));

    return (ftbl[cnt]*ftbl2[t&31])>>15;
}

Voici le code pour générer les tables:

ftbl[0]=0;
for (int i=0;i<32;i++) ftbl[i+1]=sqrt(pow(2.0,i));
printf("int ftbl[33]={0");
for (int i=0;i<32;i++) printf(",%d",ftbl[i+1]);
printf("};\n");

for (int i=0;i<32;i++) ftbl2[i]=sqrt(1.0+i/32.0)*32768;
printf("int ftbl2[32]={");
for (int i=0;i<32;i++) printf("%c%d",(i)?',':' ',ftbl2[i]);
printf("};\n");

Sur la plage 1 → 220, l'erreur maximale est de 11 et sur la plage 1 → 230, c'est environ 256. Vous pouvez utiliser des tables plus grandes et minimiser cela. Il convient de mentionner que l'erreur sera toujours négative - c'est-à-dire que lorsqu'elle est erronée, la valeur sera inférieure à la valeur correcte.

Vous feriez bien de suivre cela avec une étape de raffinage.

L'idée est assez simple: (ab)0,5 = a0.b × b0,5.

Donc, nous prenons l'entrée X = A × B où A = 2N et 1 ≤ B <2

Ensuite, nous avons une table de recherche pour sqrt (2N) et une table de recherche pour sqrt (1 ≤ B <2). Nous stockons la table de recherche pour sqrt (2N) en tant qu'entier, ce qui pourrait être une erreur (les tests ne montrent aucun effet néfaste), et nous stockons la table de recherche pour sqrt (1 ≤ B <2) en tant que virgule fixe de 15 bits.

Nous savons que 1 ≤ sqrt (2N) <65536, donc c'est 16 bits, et nous savons que nous ne pouvons vraiment multiplier que 16 bits × 15 bits sur un ARM, sans crainte de représailles, c'est ce que nous faisons.

En termes d'implémentation, la while(t) {cnt++;t>>=1;} est en fait une instruction de comptage des bits de premier plan (CLB), donc si votre version du chipset a cela, vous gagnez! De plus, l'instruction shift serait facile à implémenter avec un levier de vitesses bidirectionnel, si vous en avez un?

Il existe un algorithme Lg [N] pour compter le bit le plus élevé ici.

En termes de nombres magiques, pour changer la taille des tables, LE nombre magique pour ftbl2 Est 32, mais notez que 6 (Lg [32 ] +1) est utilisé pour le décalage.

15
Dave Gamble

Une approche courante est la bissection.

hi = number
lo = 0
mid = ( hi + lo ) / 2
mid2 = mid*mid
while( lo < hi-1 and mid2 != number ) {
    if( mid2 < number ) {
        lo = mid
    else
        hi = mid
    mid = ( hi + lo ) / 2
    mid2 = mid*mid

Quelque chose comme ça devrait fonctionner assez bien. Il fait des tests log2 (nombre), multiplie et divise log2 (nombre). Étant donné que la division est une division par 2, vous pouvez la remplacer par un >>.

La condition de fin peut ne pas être parfaite, alors assurez-vous de tester une variété d'entiers pour vous assurer que la division par 2 n'oscille pas incorrectement entre deux valeurs paires; ils différeraient de plus d'un.

8
S.Lott

Je trouve que la plupart des algorithmes sont basés sur des idées simples, mais sont implémentés de manière plus compliquée que nécessaire. J'ai pris l'idée d'ici: http://ww1.microchip.com/downloads/en/AppNotes/91040a.pdf (par Ross M. Fosler) et en ai fait un très court C -une fonction:

uint16_t int_sqrt32(uint32_t x)
{
    uint16_t res=0;
    uint16_t add= 0x8000;   
    int i;
    for(i=0;i<16;i++)
    {
        uint16_t temp=res | add;
        uint32_t g2=temp*temp;      
        if (x>=g2)
        {
            res=temp;           
        }
        add>>=1;
    }
    return res;
}

Cela se compile à 5 cycles/bit sur mon thon noir. Je pense que votre code compilé sera en général plus rapide si vous utilisez des boucles for plutôt que des boucles while, et vous obtenez l'avantage supplémentaire du temps déterministe (bien que cela dépende dans une certaine mesure de la façon dont votre compilateur optimise l'instruction if.)

7
Gutskalk

Cela dépend de l'utilisation de la fonction sqrt. J'utilise souvent quelques approximations pour faire des versions rapides. Par exemple, lorsque j'ai besoin de calculer le module de vecteur:

Module = SQRT( x^2 + y^2)

J'utilise :

Module = MAX( x,y) + Min(x,y)/2

Qui peut être codé en 3 ou 4 instructions comme:

If (x > y )
  Module  = x + y >> 1;
Else
   Module  = y + x >> 1;
7
Yazou

Ce n'est pas rapide mais c'est petit et simple:

int isqrt(int n)
{
  int b = 0;

  while(n >= 0)
  {
    n = n - b;
    b = b + 1;
    n = n - b;
  }

  return b - 1;
}
6
Philip

Je me suis installé sur quelque chose de similaire à l'algorithme binaire chiffre par chiffre décrit dans cet article Wikipedia .

3
Ber

Voici une solution en Java qui combine log_2 entier et la méthode de Newton pour créer un algorithme sans boucle. En contrepartie, il a besoin de division. Les lignes commentées sont requises pour la conversion ascendante en un algorithme 64 bits .

private static final int debruijn= 0x07C4ACDD;
//private static final long debruijn= ( ~0x0218A392CD3D5DBFL)>>>6;

static
{
  for(int x= 0; x<32; ++x)
  {
    final long v= ~( -2L<<(x));
    DeBruijnArray[(int)((v*debruijn)>>>27)]= x; //>>>58
  }
  for(int x= 0; x<32; ++x)
    SQRT[x]= (int) (Math.sqrt((1L<<DeBruijnArray[x])*Math.sqrt(2)));
}

public static int sqrt(final int num)
{
  int y;
  if(num==0)
    return num;
  {
    int v= num;
    v|= v>>>1; // first round up to one less than a power of 2 
    v|= v>>>2;
    v|= v>>>4;
    v|= v>>>8;
    v|= v>>>16;
    //v|= v>>>32;
    y= SQRT[(v*debruijn)>>>27]; //>>>58
  }
  //y= (y+num/y)>>>1;
  y= (y+num/y)>>>1;
  y= (y+num/y)>>>1;
  y= (y+num/y)>>>1;
  return y*y>num?y-1:y;
}

Comment cela fonctionne: La première partie produit une racine carrée précise à environ trois bits. La ligne y= (y+num/y)>>1; double la précision en bits. La dernière ligne élimine les racines du toit qui peuvent être générées.

1
warren

Si vous en avez besoin uniquement pour ARM Thumb 2 processeurs, la bibliothèque CMSIS DSP de ARM est la meilleure solution pour vous). Il est fait par des personnes qui ont conçu des processeurs Thumb 2. Qui d'autre peut le battre?

En fait, vous n'avez même pas besoin d'un algorithme, mais d'instructions matérielles spécialisées de racine carrée telles que VSQRT . La société ARM maintient des implémentations d'algorithmes mathématiques et DSP hautement optimisées pour les processeurs pris en charge par Thumb 2 en essayant d'utiliser son matériel comme VSQRT. Vous pouvez obtenir le code source:

Notez que ARM maintient également des binaires compilés de CMSIS DSP qui garantissent les meilleures performances possibles pour ARM Thumb instructions spécifiques à l'architecture. Vous devez donc envisager de les lier statiquement lorsque vous utilisez la bibliothèque. Vous pouvez obtenir les binaires ici.

0
Bumsik Kim

Cette méthode est similaire à la division longue: vous construisez une supposition pour le prochain chiffre de la racine, effectuez une soustraction et entrez le chiffre si la différence répond à certains critères. Avec la version binaire, votre seul choix pour le chiffre suivant est 0 ou 1, donc vous devinez toujours 1, effectuez la soustraction et entrez 1 sauf si la différence est négative.

http://www.realitypixels.com/turk/opensource/index.html#FractSqrt

0
Ken Turkowski

J'ai récemment rencontré la même tâche sur ARM Cortex-M3 (STM32F103CBT6) et après avoir recherché Internet, j'ai trouvé la solution suivante. Ce n'est pas la comparaison la plus rapide avec les solutions proposées ici, mais elle a une bonne précision (l'erreur maximale est de 1, c'est-à-dire LSB sur toute la plage d'entrée UI32) et une vitesse relativement bonne (environ 1,3 M de racines carrées par seconde sur un 72 MHz ARM Cortex-M3 ou environ 55 cycles par racine unique, y compris l'appel de fonction).

// FastIntSqrt is based on Wikipedia article:
// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots
// Which involves Newton's method which gives the following iterative formula:
//
// X(n+1) = (X(n) + S/X(n))/2
//
// Thanks to ARM CLZ instruction (which counts how many bits in a number are
// zeros starting from the most significant one) we can very successfully
// choose the starting value, so just three iterations are enough to achieve
// maximum possible error of 1. The algorithm uses division, but fortunately
// it is fast enough here, so square root computation takes only about 50-55
// cycles with maximum compiler optimization.
uint32_t FastIntSqrt (uint32_t value)
{
    if (!value)
        return 0;

    uint32_t xn = 1 << ((32 - __CLZ (value))/2);
    xn = (xn + value/xn)/2;
    xn = (xn + value/xn)/2;
    xn = (xn + value/xn)/2;
    return xn;
}

J'utilise IAR et il produit le code assembleur suivant:

        SECTION `.text`:CODE:NOROOT(1)
        THUMB
_Z11FastIntSqrtj:
        MOVS     R1,R0
        BNE.N    ??FastIntSqrt_0
        MOVS     R0,#+0
        BX       LR
??FastIntSqrt_0:
        CLZ      R0,R1
        RSB      R0,R0,#+32
        MOVS     R2,#+1
        LSRS     R0,R0,#+1
        LSL      R0,R2,R0
        UDIV     R3,R1,R0
        ADDS     R0,R3,R0
        LSRS     R0,R0,#+1
        UDIV     R2,R1,R0
        ADDS     R0,R2,R0
        LSRS     R0,R0,#+1
        UDIV     R1,R1,R0
        ADDS     R0,R1,R0
        LSRS     R0,R0,#+1
        BX       LR               ;; return
0
Kde