GCC kompiliert die Zählung der führenden Nullen schlecht, sofern nicht Haswell angegeben wurde

9

Der GCC unterstützt die __builtin_clz(int x) builtin, die die Anzahl der Anzahl der führenden Nullen (aufeinanderfolgende höchstwertige Nullen) im Argument zählt.

Dies ist unter anderem 0 , um die Funktion lg(unsigned int x) effizient zu implementieren, die den Logarithmus zur Basis 2 von x , Abrundung 1 , verwendet:

%Vor%

Das funktioniert auf einfache Art und Weise - bedenke insbesondere den Fall x == 1 und clz(x) == 31 - dann x == 2^0 so lg(x) == 0 und 31 - 31 == 0 und wir erhalten das korrekte Ergebnis. Höhere Werte von x funktionieren ähnlich.

Unter der Annahme, dass das Built-in effizient implementiert ist, endet es viel besser als die alternativen reinen C-Lösungen .

Nun ist die Operation führende Nullen zählen im Wesentlichen das Duale des bsr Anweisung in x86. Dies gibt den Index des höchstwertigen 1-Bit 2 in dem Argument zurück. Wenn also 10 führende Nullen vorhanden sind, befindet sich das erste 1-Bit in Bit 21 des Arguments. Im Allgemeinen haben wir 31 - clz(x) == bsr(x) und in so bsr tatsächlich direkt implementiert unsere gewünschte lg() -Funktion, ohne den überflüssigen 31U - ... -Teil.

Tatsächlich können Sie zwischen der Zeile lesen und sehen, dass die Funktion __builtin_clz mit bsr implementiert wurde: Sie ist definiert als undefiniertes Verhalten , wenn das Argument Null ist, wenn of Natürlich ist die Operation "führende Nullen" perfekt als 32 (oder was auch immer die Bitgröße von int ist) mit einem Nullargument definiert. So wurde __builtin_clz sicherlich mit der Idee implementiert, effizient auf eine bsr Anweisung auf x86 abgebildet zu werden.

Betrachtet man jedoch, was GCC tatsächlich macht, bei -O3 mit anderen Standardoptionen: fügt eine Menge zusätzlichen Junk hinzu :

%Vor%

Die xor edi,31 -Zeile ist effektiv ein not edi für die unteren 4 Bits, die wirklich wichtig sind, das ist off-by-one 3 von neg edi , was das Ergebnis von bsr in verwandelt %Code%. Dann wird clz ausgeführt.

Jedoch mit 31 - clz(x) vereinfacht der Code genau die erwartete Anweisung -mtune=haswell :

%Vor%

Warum das der Fall ist, ist mir sehr unklar. Der Befehl bsr gibt es seit ein paar Jahrzehnten vor Haswell, und das Verhalten ist, AFAIK, unverändert. Es ist nicht nur ein Problem von tuning für einen bestimmten arch, da bsr + eine Menge zusätzlicher Anweisungen nicht schneller ist als eine einfache bsr und weiterhin bsr führt immer noch zu im langsameren Code.

Die Situation für 64-bit Ein- und Ausgaben ist sogar etwas schlechter : Es gibt ein Extra -mtune=haswell im kritischen Pfad, was nichts zu tun scheint, da das Ergebnis von movsx niemals negativ sein wird. Auch hier ist die clz Variante optimal mit einer einzelnen -march=haswell Anweisung.

Betrachten wir abschließend die großen Player im Nicht-Windows-Compiler-Bereich bsr und icc . clang macht nur einen schlechten Job und fügt redundantes Zeug wie icc - wtf hinzu? neg eax; add eax, 31; neg eax; add eax, 31; macht einen guten Job unabhängig von clang .

0 So wie das Scannen einer Bitmap für das erste gesetzte Bit.

1 Der Logarithmus von 0 ist nicht definiert, und daher ist der Aufruf unserer Funktion mit einem 0-Argument undefiniertes Verhalten .

2 Hier ist das LSB das 0. Bit und das MSB ist das 31. Bit.

3 Erinnern Sie sich, dass -march in Zweierkomplement.

    
BeeOnRope 07.11.2016, 01:17
quelle

1 Antwort

7

Das sieht nach einem bekannten Problem mit gcc aus: Ссылка

    
Mark Lakata 07.11.2016 05:29
quelle

Tags und Links