web-dev-qa-db-fra.com

Calcul rapide de log2 pour les entiers 64 bits

Une excellente ressource de programmation, Bit Twiddling Hacks, propose ( ici ) la méthode suivante pour calculer log2 d'un entier 32 bits:

#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const char LogTable256[256] = 
{
    -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
    LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6),
    LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7)
};

unsigned int v; // 32-bit Word to find the log of
unsigned r;     // r will be lg(v)
register unsigned int t, tt; // temporaries
if (tt = v >> 16)
{
    r = (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
}
else 
{
    r = (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
}

et mentionne que

La méthode de la table de recherche ne prend qu'environ 7 opérations pour trouver le journal d'une valeur 32 bits. S'il était étendu pour des quantités 64 bits, il faudrait environ 9 opérations.

mais, hélas, ne donne aucune information supplémentaire sur la voie à suivre pour étendre l'algorithme aux entiers 64 bits.

Avez-vous des conseils sur l'apparence d'un algorithme 64 bits de ce type?

41
Desmond Hume

Les fonctions intrinsèques sont vraiment rapides mais restent insuffisantes pour une implémentation véritablement multiplateforme et indépendante du compilateur de log2. Donc, si quelqu'un est intéressé, voici l'algorithme de type DeBruijn le plus rapide, sans branche et sans CPU, auquel je suis arrivé tout en recherchant le sujet par moi-même.

const int tab64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5};

int log2_64 (uint64_t value)
{
    value |= value >> 1;
    value |= value >> 2;
    value |= value >> 4;
    value |= value >> 8;
    value |= value >> 16;
    value |= value >> 32;
    return tab64[((uint64_t)((value - (value >> 1))*0x07EDD5E59A4E28C2)) >> 58];
}

La partie de l'arrondi à la puissance inférieure suivante de 2 a été tirée de Limites de puissance de 2 et la partie de l'obtention du nombre de zéros de fin a été prise de BitScan ( les (bb & -bb) le code permet de distinguer le bit le plus à droite défini sur 1, ce qui n'est pas nécessaire après avoir arrondi la valeur à la puissance suivante de 2).

Et l'implémentation 32 bits, soit dit en passant, est

const int tab32[32] = {
     0,  9,  1, 10, 13, 21,  2, 29,
    11, 14, 16, 18, 22, 25,  3, 30,
     8, 12, 20, 28, 15, 17, 24,  7,
    19, 27, 23,  6, 26,  5,  4, 31};

int log2_32 (uint32_t value)
{
    value |= value >> 1;
    value |= value >> 2;
    value |= value >> 4;
    value |= value >> 8;
    value |= value >> 16;
    return tab32[(uint32_t)(value*0x07C4ACDD) >> 27];
}

Comme pour toute autre méthode de calcul, log2 requiert que la valeur d'entrée soit supérieure à zéro.

62
Desmond Hume

Si vous utilisez GCC, une table de recherche n'est pas nécessaire dans ce cas.

GCC fournit une fonction intégrée pour déterminer la quantité de zéros non significatifs:

Fonction intégrée: int __builtin_clz (unsigned int x)
Renvoie le nombre de 0 bits de début en x, en commençant à la position de bit la plus significative. Si x vaut 0, le résultat n'est pas défini.

Vous pouvez donc définir:

#define LOG2(X) ((unsigned) (8*sizeof (unsigned long long) - __builtin_clzll((X)) - 1))

et cela fonctionnera pour tout entier long long non signé. Le résultat est arrondi.

Pour x86 et AMD64, GCC le compilera en une instruction bsr , donc la solution est très rapide (beaucoup plus rapide que les tables de recherche).

Exemple de travail :

#include <stdio.h>

#define LOG2(X) ((unsigned) (8*sizeof (unsigned long long) - __builtin_clzll((X)) - 1))

int main(void) {
    unsigned long long input;
    while (scanf("%llu", &input) == 1) {
        printf("log(%llu) = %u\n", input, LOG2(input));
    }
    return 0;
}
44
kay

J'essayais de convertir Trouver la base de journal 2 d'un entier de N bits dans O(lg(N)) opérations avec multiplication et recherche à 64 bits par brute forçant le nombre magique. Inutile de dire que cela prenait un certain temps.

J'ai ensuite trouvé la réponse de Desmond et décidé d'essayer son nombre magique comme point de départ. Étant donné que j'ai un processeur à 6 cœurs, je l'ai exécuté en parallèle à partir de multiples de 0x07EDDregular9A4E28C2/6. J'ai été surpris qu'il ait trouvé quelque chose immédiatement. Il s'avère que 0x07EDDregular9A4E28C2/2 a fonctionné.

Voici donc le code pour 0x07EDDregular9A4E28C2 qui vous évite un décalage et une soustraction:

int LogBase2(uint64_t n)
{
    static const int table[64] = {
        0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61,
        51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62,
        57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56,
        45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 63 };

    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    n |= n >> 32;

    return table[(n * 0x03f6eaf2cd271461) >> 58];
}
16
Avernar

Logarithme de base 2

Voici ce que je fais pour les entiers non signés 64 bits. Ceci calcule le plancher du logarithme en base 2, qui est équivalent à l'indice du bit le plus significatif. Cette méthode est extrêmement rapide pour les grands nombres car elle utilise une boucle non déroulée qui s'exécute toujours en log₂64 = 6 étapes.

Essentiellement, il soustrait des carrés progressivement plus petits dans la séquence {0 ≤ k ≤ 5: 2 ^ (2 ^ k)} = {2³², 2¹⁶, 2⁸, 2⁴, 2², 2¹} = {4294967296, 65536, 256 , 16, 4, 2, 1} et additionne les exposants k des valeurs soustraites.

int uint64_log2(uint64_t n)
{
  #define S(k) if (n >= (UINT64_C(1) << k)) { i += k; n >>= k; }

  int i = -(n == 0); S(32); S(16); S(8); S(4); S(2); S(1); return i;

  #undef S
}

Notez que cela retourne -1 si donné l'entrée invalide de 0 (qui est ce que la -(n == 0) initiale vérifie). Si vous ne vous attendez jamais à l'invoquer avec n == 0, Vous pouvez remplacer int i = 0; Pour l'initialiseur et ajouter assert(n != 0); à l'entrée de la fonction.

Logarithme entier en base 10

Les logarithmes entiers en base 10 peuvent être calculés de la même manière - le plus grand carré à tester étant 10¹⁶ car log₁₀2⁶⁴ ≅ 19.2659 ... (Remarque: ce n'est pas le moyen le plus rapide pour réaliser un logarithme entier en base 10, car il utilise la division entière, qui est intrinsèquement lent. Une implémentation plus rapide consisterait à utiliser un accumulateur avec des valeurs qui croissent de façon exponentielle, et à comparer avec l'accumulateur, en faisant en fait une sorte de recherche binaire.)

int uint64_log10(uint64_t n)
{
  #define S(k, m) if (n >= UINT64_C(m)) { i += k; n /= UINT64_C(m); }

  int i = -(n == 0);
  S(16,10000000000000000); S(8,100000000); S(4,10000); S(2,100); S(1,10);
  return i;

  #undef S
}
8
Todd Lehman

Voici une extension assez compacte et rapide, n'utilisant pas de temporaires supplémentaires:

r = 0;

/* If its wider than 32 bits, then we already know that log >= 32.
So store it in R.  */
if (v >> 32)
  {
    r = 32;
    v >>= 32;
  }

/* Now do the exact same thing as the 32 bit algorithm,
except we ADD to R this time.  */
if (tt = v >> 16)
  {
    r += (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
  }
else
  {
    r += (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
  }

En voici un construit avec une chaîne de ifs, encore une fois sans temporisation supplémentaire. Mais ce n'est peut-être pas le plus rapide.

  if (tt = v >> 48)
    {
      r = (t = tt >> 8) ? 56 + LogTable256[t] : 48 + LogTable256[tt];
    }
  else if (tt = v >> 32)
    {
      r = (t = tt >> 8) ? 40 + LogTable256[t] : 32 + LogTable256[tt];
    }
  else if (tt = v >> 16)
    {
      r = (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
    }
  else 
    {
      r = (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
    }
4
ArjunShankar

L'algorithme recherche essentiellement l'octet qui contient le bit le plus significatif, puis recherche cet octet dans la recherche pour trouver le journal de l'octet, puis l'ajoute à la position de l'octet.

Voici une version quelque peu simplifiée de l'algorithme 32 bits:

if (tt = v >> 16)
{
    if (t = tt >> 8)
    {
        r = 24 + LogTable256[t];
    }
    else
    {
        r = 16 + LogTable256[tt];
    }
}
else 
{
    if (t = v >> 8)
    {
        r = 8 + LogTable256[t];
    }
    else
    {
        r = LogTable256[v];
    }
}

Il s'agit de l'algorithme 64 bits équivalent:

if (ttt = v >> 32)
{
    if (tt = ttt >> 16)
    {
        if (t = tt >> 8)
        {
            r = 56 + LogTable256[t];
        }
        else
        {
            r = 48 + LogTable256[tt];
        }
    }
    else 
    {
        if (t = ttt >> 8)
        {
            r = 40 + LogTable256[t];
        }
        else
        {
            r = 32 + LogTable256[ttt];
        }
    }
}
else
{
    if (tt = v >> 16)
    {
        if (t = tt >> 8)
        {
            r = 24 + LogTable256[t];
        }
        else
        {
            r = 16 + LogTable256[tt];
        }
    }
    else 
    {
        if (t = v >> 8)
        {
            r = 8 + LogTable256[t];
        }
        else
        {
            r = LogTable256[v];
        }
    }
}

J'ai trouvé un algorithme pour tous les types de taille, je pense que c'est plus agréable que l'original.

unsigned int v = 42;
unsigned int r = 0;
unsigned int b;
for (b = sizeof(v) << 2; b; b = b >> 1)
{
    if (v >> b)
    {
        v = v >> b;
        r += b;
    }
}

Remarque: b = sizeof(v) << 2 définit b à la moitié du nombre de bits dans v. J'ai utilisé le décalage au lieu de la multiplication ici (juste parce que j'en avais envie).

Vous pouvez ajouter une table de recherche à cet algorithme pour l'accélérer éventuellement, mais il s'agit plutôt d'une preuve de concept.

2
Kendall Frey

Prends ça:

typedef unsigned int uint;
typedef uint64_t ulong;
uint as_uint(const float x) {
    return *(uint*)&x;
}
ulong as_ulong(const double x) {
    return *(ulong*)&x;
}
uint log2_fast(const uint x) {
    return (as_uint((float)x)>>23)-127;
}
uint log2_fast(const ulong x) {
    return (uint)((as_ulong((double)x)>>52))-1023;
}

Fonctionnement: l'entier d'entrée x est converti en float puis réinterprété en bits. Le format IEEE float stocke l'exposant dans les bits 30-23 sous la forme d'un entier avec un biais 127, donc en le décalant de 23 bits vers la droite et en soustrayant le biais, nous obtenons log2 (x). Pour une entrée entière 64 bits, x est converti en double, pour lequel l'exposant est dans les bits 62-52 (décalage de 52 bits vers la droite) et le biais d'exposant est 1023.

0
ProjectPhysX