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.
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;
}
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.
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.
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.)
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;
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;
}
Je me suis installé sur quelque chose de similaire à l'algorithme binaire chiffre par chiffre décrit dans cet article Wikipedia .
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.
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:
arm_sqrt_f32()
arm_sqrt_q15.c
/arm_sqrt_q31.c
(q15 et q31 sont les types de données à virgule fixe spécialisés pour ARM noyau DSP, qui est souvent fourni avec des processeurs compatibles Thum 2).)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.
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
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