Schnelle Berechnung von log2 für 64-Bit-Ganzzahlen

Lesezeit: 13 Minuten

Eine großartige Programmierressource, Bit Twddling Hacks, schlägt vor (hier) die folgende Methode, um log2 einer 32-Bit-Ganzzahl zu berechnen:

#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
}
else 
{
    r = (t = v >> 8) ? 8 + LogTable256
}

und erwähnt das

Die Lookup-Table-Methode benötigt nur etwa 7 Operationen, um das Protokoll eines 32-Bit-Werts zu finden. Wenn es für 64-Bit-Mengen erweitert würde, würde es ungefähr 9 Operationen dauern.

gibt aber leider keine zusätzlichen Informationen darüber, welchen Weg man tatsächlich gehen sollte, um den Algorithmus auf 64-Bit-Ganzzahlen zu erweitern.

Irgendwelche Hinweise darauf, wie ein solcher 64-Bit-Algorithmus aussehen würde?

  • @dbaupp Ich habe jede Menge davon ifs aller möglichen Arten, Sorten und Geschmacksrichtungen, welche eignen sich am besten?

    – Desmond Hume

    7. Juli 2012 um 15:41 Uhr


  • Das ist nur eine akademische Frage, oder? Ansonsten einfach verwenden _BitScanReverse64 (msvc) bzw __builtin_clzll (ggc)

    – Harald

    7. Juli 2012 um 15:42 Uhr

  • Solche wie die, die Sie bereits haben. (Mit der naivsten Erweiterung sieht es ungefähr so ​​​​aus if (tt = v >> 48) { ... } else if (tt = v >> 32) { ... } ...obwohl dies geringfügig schlechter abschneidet als die richtige binäre Suche, die Kendall korrekt vorschlägt.)

    – huon

    7. Juli 2012 um 15:43 Uhr


  • @harold: Nein, das ist überhaupt keine akademische Frage. Selbst wenn sich jemand entscheidet, Compiler-spezifische Intrinsiken zu verwenden, werden sie auf Compiler-spezifisch eingehen #if Geäst. Dies beseitigt natürlich nicht die Notwendigkeit eines mehr oder weniger universell implementierten “Standard”-Zweigs.

    – AnT steht zu Russland

    7. Juli 2012 um 16:39 Uhr

  • @AndreyT es wäre interessant, wenn die Leute damit anfangen würden. Code könnte tatsächlich real-life-portabel werden, anstatt Ivory-Tower-portabel (wobei man sich nicht auf eine vernünftige Implementierung von int verlassen kann, sondern auf eine gcc-spezifische Spracherweiterung kann)

    – Harald

    7. Juli 2012 um 16:59 Uhr

Benutzeravatar von Desmond Hume
Desmond Hume

Intrinsische Funktionen sind wirklich schnell, aber für eine wirklich plattformübergreifende, Compiler-unabhängige Implementierung von log2 noch unzureichend. Falls es also jemanden interessiert, hier ist der schnellste, verzweigungsfreie, CPU-abstrakte DeBruijn-ähnliche Algorithmus, auf den ich bei meinen eigenen Recherchen zu diesem Thema gestoßen bin.

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

Der Teil des Abrundens auf die nächstniedrigere Potenz von 2 wurde aus entnommen Potenz-von-2-Grenzen und der Teil, um die Anzahl der nachgestellten Nullen zu erhalten, wurde entnommen BitScan (das (bb & -bb) Code, um das Bit ganz rechts herauszuheben, das auf 1 gesetzt ist, was nicht benötigt wird, nachdem wir den Wert auf die nächste Potenz von 2 abgerundet haben).

Und die 32-Bit-Implementierung ist es übrigens

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

Wie bei jeder anderen Berechnungsmethode erfordert log2, dass der Eingabewert größer als Null ist.

  • @ArjunShankar Gern geschehen. Ich denke jedoch, dass es eine Möglichkeit gibt, diese beiden Operationen in der Tabellensuchzeile, nämlich Subtraktion und Rechtsverschiebung, durch Generieren einer weiteren perfekten Hash-Funktion zu reduzieren. Ich weiß nicht, ob ich genug Zeit für diese Verfolgung von Zen haben werde, solange meine Hauptcompiler MSVC und GCC sind;)

    – Desmond Hume

    9. Juli 2012 um 16:29 Uhr


  • @ArjunShankar Und um zu verdeutlichen, woher die Operationen, Tabelleneinträge und Konstanten stammen, habe ich die Antwort aktualisiert, um die Quellen zu zitieren.

    – Desmond Hume

    9. Juli 2012 um 16:50 Uhr


  • Um beim Kompromiss zwischen Portabilität und Geschwindigkeit zu helfen, auf einer Intel(R) Xeon(R) CPU X5650 @ 2,67 GHz Die Nachschlagetabelle ist im Durchschnitt etwa 4-mal langsamer als die intrinsische (etwa 13 Zyklen gegenüber 4 Zyklen).

    – Komm Raczy

    15. August 2013 um 13:23 Uhr

  • @DesmondHume – Da der Eingabewert, wie Sie sagen, größer als Null sein muss, möchten Sie vielleicht eine Zeile am Anfang der Funktion hinzufügen, die besagt assert(value > 0);. Dies dient nicht nur der Fehlersuche, sondern auch der Dokumentation der Eingabebedingungen.

    – Todd Lehmann

    21. April 2016 um 14:39 Uhr


  • @DesmondHume – Eine weitere Beobachtung: Anstatt die tab64[] Tabelle enthalten int Werte, warum nicht 8-Bit-Werte enthalten und die Tabelle angeben als const int8_t tab64[64]… Auf diese Weise benötigt die Tabelle nur noch 64 Bytes (statt 256). Zusätzlich, wenn Sie einschließen __attribute__((aligned(64))), dann passt es auch garantiert in eine einzelne Cache-Zeile auf einem modernen Prozessor. Wie derzeit geschrieben (mit 256 Bytes und ohne Ausrichtung), könnte die Tabelle bis zu 5 Cache-Zeilen verwenden.

    – Todd Lehmann

    21. April 2016 um 14:44 Uhr


Benutzeravatar von kay
kay

Wenn Sie GCC verwenden, ist eine Nachschlagetabelle in diesem Fall nicht erforderlich.

GCC bietet eine eingebaute Funktion, um die Anzahl der führenden Nullen zu bestimmen:

Eingebaute Funktion: int __builtin_clz (unsigned int x)

Gibt die Anzahl der führenden 0-Bits in x zurück, beginnend bei der höchstwertigen Bitposition. Wenn x 0 ist, ist das Ergebnis undefiniert.

Sie können also definieren:

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

und es funktioniert für alle unsigned long long int. Das Ergebnis wird abgerundet.

Für x86 und AMD64 kompiliert GCC es zu a bsr Anweisung, daher ist die Lösung sehr schnell (viel schneller als Nachschlagetabellen).

Arbeitsbeispiel:

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

Kompilierte Ausgabe: https://godbolt.org/z/16GnjszMs

  • Wie wäre es auch mit “Wenn x 0 ist, ist das Ergebnis undefiniert.” in deinem beispiel? 🙂

    – ArjunShankar

    7. Juli 2012 um 16:41 Uhr


  • @ArjunShankar eigentlich habe ich darüber nachgedacht, konnte mir aber keine geeignete Ganzzahl für diesen Fall vorstellen. 😉 Ich überlasse es dem interessierten Leser, dem Makro einen if-else-Fall hinzuzufügen. (Auch nur das Ergebnis wird undefiniert sein [most likely 0]aber es wird keinen Absturz geben, wenn eine 0 angegeben wurde.)

    – kay

    7. Juli 2012 um 16:46 Uhr

  • @kay Wusste nichts davon bsr Anweisung. Ich wollte, dass es CPU-unabhängiger ist. Vielen Dank.

    – Desmond Hume

    7. Juli 2012 um 17:27 Uhr


  • @DesmondHume __builtin_clz ist nicht prozessorspezifisch. GCC findet eine Reihe von Anweisungen, die für jede unterstützte Architektur gut funktionieren.

    – kay

    7. Juli 2012 um 17:36 Uhr

  • Wenn Sie für Haswell oder höher kompilieren, wählen Sie GCC und Clang aus LZCNT Anstatt von BSR. Clang dann sogar erzeugt nur lzcnt rax, rdi; xor eax, 63 wohingegen GCC oder ICC 1 oder 2 zusätzliche Anweisungen für diese ganzzahlige log2-Funktion auswählen.

    – maxschlepzig

    24. September 2020 um 10:49 Uhr

Ich habe versucht zu konvertieren Ermitteln Sie die logarithmische Basis 2 einer N-Bit-Ganzzahl in O(lg(N))-Operationen mit Multiplizieren und Nachschlagen auf 64-Bit durch Brute Force der magischen Zahl. Unnötig zu erwähnen, dass es eine Weile gedauert hat.

Dann fand ich Desmonds Antwort und beschloss, seine magische Zahl als Ausgangspunkt zu versuchen. Da ich einen 6-Kern-Prozessor habe, habe ich ihn parallel ausgeführt, beginnend bei 0x07EDD5E59A4E28C2 / 6-Vielfachen. Ich war überrascht, dass es sofort etwas gefunden hat. Es stellt sich heraus, dass 0x07EDD5E59A4E28C2 / 2 funktioniert hat.

Hier ist also der Code für 0x07EDD5E59A4E28C2, der Ihnen eine Verschiebung und Subtraktion erspart:

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

  • Bist du dir zu 100% sicher, dass das in allen Fällen richtig ist? Ich habe mich noch nicht wirklich damit beschäftigt, wie das intern funktioniert, also weiß ich nicht, ob es eine Möglichkeit gibt, die Korrektheit zu beweisen …

    – Markus A.

    3. Oktober 2014 um 17:54 Uhr

  • Es ist für alle Eingaben außer 0 korrekt. Es gibt 0 für 0 zurück, was für das, wofür Sie es verwenden, gültig sein kann. Die Zeilen mit den Verschiebungen runden n auf 1 kleiner als die nächste Potenz von 2 auf. Es setzt grundsätzlich alle Bits nach dem führenden 1-Bit auf 1. Dies reduziert alle möglichen Eingaben auf 64 mögliche Werte: 0x0, 0x1, 0x3, 0x7, 0xf , 0x1f, 0x3f usw. Wenn Sie diese 64 Werte mit der Zahl 0x03f6eaf2cd271461 multiplizieren, erhalten Sie weitere 64 eindeutige Werte in den oberen 6 Bits. Die Verschiebung um 58 positioniert nur diese 6 Bits zur Verwendung als Index in der Tabelle.

    – Avenar

    7. Oktober 2014 um 1:27 Uhr

  • Das macht absolut Sinn. Vielen Dank. Irgendwie hatte ich eine Gehirnblockade und ich las den Code als n = n | (n >> 1) | (n >> 2) und so weiter und versuchte herauszufinden, wie viele Fälle ich überprüfen müsste, um die Korrektheit zu überprüfen … D’Uh … Immer gut zu wissen warum etwas funktioniert! 🙂 +1

    – Markus A.

    7. Oktober 2014 um 3:24 Uhr

  • @Avernar – Wie ich gerade Desmond Hume in seiner fast identischen Lösung aufgezeigt habe, wenn Sie Ihre Tabelle dazu bringen, Elemente des Typs zu enthalten int8_t Anstatt von intund richten Sie sie an einer 64-Byte-Grenze aus, dann ist die Tabelle nur noch 64 Bytes statt 256 Bytes groß und passt auf modernen Prozessoren in eine einzelne Cache-Zeile.

    – Todd Lehmann

    21. April 2016 um 14:49 Uhr

  • Ich denke, das funktioniert, weil deine “magische Zahl” m ist ein DeBruin-Sequenz B(2, 6), das mit sechs beginnt 0s gefolgt von sechs 1s. Bei einer solchen Folge multipliziert man mit ns des Formulars 00011111 Anstatt von 00010000 funktioniert noch. Jetzt steht die Operation an m * (2^(n+1) - 1)oder m * 2^(n+1) - mAnstatt von m * 2^n. Da die Sequenz mit sechs beginnt 0s, multipliziert mit 2^(n+1) ergibt immer noch einen perfekten Hasch. Wegen der folgenden sechs 1sdie Subtraktion stets propagiert ein Übertragsbit in die oberen sechs Bits des Ergebnisses.

    – nwellnhof

    30. Mai 2017 um 0:29 Uhr

Benutzeravatar von Todd Lehman
Tod Lehmann

Ganzzahliger Logarithmus zur Basis 2

Folgendes mache ich für 64-Bit-Ganzzahlen ohne Vorzeichen. Dies berechnet den Boden des Basis-2-Logarithmus, der dem Index des höchstwertigen Bits entspricht. Diese Methode ist rauchend schnell für große Zahlen, weil es eine ungerollte Schleife verwendet, die immer in log₂64 = 6 Schritten ausgeführt wird.

Im Wesentlichen subtrahiert es zunehmend kleinere Quadrate in der Folge { 0 ≤ k ≤ 5: 2^(2^k) } = { 2³², 2¹⁶, 2⁸, 2⁴, 2², 2¹ } = { 4294967296, 65536, 256 , 16, 4, 2, 1 } und summiert die Exponenten k der subtrahierten Werte.

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
}

Beachten Sie, dass dies –1 zurückgibt, wenn die ungültige Eingabe von 0 gegeben wird (was die anfängliche -(n == 0) sucht nach). Wenn Sie nie erwarten, es mit aufzurufen n == 0könnten Sie ersetzen int i = 0; für den Initialisierer und hinzufügen assert(n != 0); beim Einstieg in die Funktion.

Ganzzahliger Logarithmus zur Basis 10

Ganzzahlige Logarithmen zur Basis 10 können auf ähnliche Weise berechnet werden – wobei das größte zu testende Quadrat 10¹⁶ ist, weil log₁₀2⁶⁴ ≅ 19,2659… was von Natur aus langsam ist. Eine schnellere Implementierung wäre die Verwendung eines Akkumulators mit Werten, die exponentiell wachsen, und ein Vergleich mit dem Akkumulator, wodurch eine Art binäre Suche durchgeführt wird.)

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
}

Benutzeravatar von ArjunShankar
ArjunShankar

Hier ist eine ziemlich kompakte und schnelle Verlängerung ohne zusätzliche Provisorien:

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
  }
else
  {
    r += (t = v >> 8) ? 8 + LogTable256
  }

Hier ist einer mit einer Kette von gebaut ifs, wieder ohne zusätzliche Provisorien. Ist aber vielleicht nicht der schnellste.

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

  • (unsigned long long oder uint64_t)

    – huon

    7. Juli 2012 um 15:58 Uhr

  • @dbaupp – Ich war einfach zu faul, um eine zu schreiben main und beinhalten stdint.h. Danke für den Schubs. Ich habe es in der Zwischenzeit ausprobiert, und es funktioniert gut.

    – ArjunShankar

    7. Juli 2012 um 16:02 Uhr

  • @ArjunShankar Der erste Algorithmus ist zweifellos großartig. Vielen Dank.

    – Desmond Hume

    7. Juli 2012 um 16:53 Uhr

Benutzeravatar von Tony Delroy
Toni Delroy

Wenn Sie nach der C++-Antwort gesucht haben und hierher gekommen sind, und da es darauf hinausläuft, Nullen zu zählen, dann haben Sie die std::countl_zero was laut godbolt.org anruft bsr.
std::countl_zero ab C++20 verfügbar ist, müssen Sie möglicherweise hinzufügen -std=gnu++2a zu Ihrer Compiler-Befehlszeile

  • (unsigned long long oder uint64_t)

    – huon

    7. Juli 2012 um 15:58 Uhr

  • @dbaupp – Ich war einfach zu faul, um eine zu schreiben main und beinhalten stdint.h. Danke für den Schubs. Ich habe es in der Zwischenzeit ausprobiert, und es funktioniert gut.

    – ArjunShankar

    7. Juli 2012 um 16:02 Uhr

  • @ArjunShankar Der erste Algorithmus ist zweifellos großartig. Vielen Dank.

    – Desmond Hume

    7. Juli 2012 um 16:53 Uhr

Der Algorithmus findet grundsätzlich heraus, welches Byte das höchstwertige 1 Bit enthält, und sucht dann dieses Byte in der Suche nach dem Protokoll des Bytes und fügt es dann der Position des Bytes hinzu.

Hier ist eine etwas vereinfachte Version des 32-Bit-Algorithmus:

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

Dies ist der entsprechende 64-Bit-Algorithmus:

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

Ich habe einen Algorithmus für alle Größentypen entwickelt, der meiner Meinung nach schöner ist als das 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;
    }
}

Notiz: b = sizeof(v) << 2 setzt b auf die Hälfte der Anzahl von Bits in v. Ich habe hier Verschiebung statt Multiplikation verwendet (nur weil ich Lust dazu hatte).

Sie könnten diesem Algorithmus eine Nachschlagetabelle hinzufügen, um ihn möglicherweise zu beschleunigen, aber es ist eher ein Proof-of-Concept.

  • Nur persönlich finde ich die kompaktere ternäre Version “einfacher”: nimmt weniger Platz ein. 🙂

    – huon

    7. Juli 2012 um 15:45 Uhr

  • @dbaupp: Hängt von deiner Sichtweise ab. Ich habe die Dreiergruppe erweitert, damit man besser sehen kann, was vor sich geht.

    – Kendall Frey

    7. Juli 2012 um 15:48 Uhr

  • @KendallFrey Danke, aber könnte die vierte Tabellensuche, wenn sie vom Anfang des 64-Bit-Algorithmus zählt, die Grenzen der Tabelle überschreiten?

    – Desmond Hume

    7. Juli 2012 um 15:53 ​​Uhr


  • @DesmondHume: Ja, ich glaube schon. Copy-n-Paste-Fehler hier. Fest.

    – Kendall Frey

    7. Juli 2012 um 15:56 Uhr

1413790cookie-checkSchnelle Berechnung von log2 für 64-Bit-Ganzzahlen

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy