Ich suche nach einem effizienten Weg, um die Position des niedrigstwertigen Bits zu bestimmen, das in einer ganzen Zahl gesetzt ist, z. B. für 0x0FF0 wäre es 4.
Eine triviale Implementierung ist folgende:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Irgendwelche Ideen, wie man ein paar Zyklen herausquetscht?
(Hinweis: Diese Frage richtet sich an Personen, die solche Dinge mögen, und nicht an Personen, die mir sagen, dass Xyzoptimierung böse ist.)
[Bearbeiten] Vielen Dank an alle für die Ideen! Ich habe noch ein paar andere Dinge gelernt. Cool!
c++
c
optimization
bit-manipulation
peterchen
quelle
quelle
Antworten:
Bit Twiddling Hacks bietet eine exzellente Sammlung von Bit Twiddling Hacks mit beigefügter Diskussion zu Leistung und Optimierung. Meine Lieblingslösung für Ihr Problem (von dieser Site) ist «Multiplizieren und Nachschlagen»:
Hilfreiche Referenzen:
quelle
__builtin_ffsl
oder istffsl
?Warum nicht das eingebaute ffs verwenden ? (Ich habe mir eine Manpage von Linux geholt, aber sie ist weiter verbreitet.)
quelle
Es gibt eine x86-Assembly-Anweisung (
bsf
), die dies ausführt. :) :)Optimierter?!
Randnotiz:
Die Optimierung auf dieser Ebene ist von Natur aus architekturabhängig. Die heutigen Prozessoren sind zu komplex (in Bezug auf Verzweigungsvorhersage, Cache-Fehler, Pipelining), als dass es so schwierig ist, vorherzusagen, welcher Code auf welcher Architektur schneller ausgeführt wird. Das Verringern von Vorgängen von 32 auf 9 oder ähnliches kann bei einigen Architekturen sogar die Leistung verringern. Optimierter Code auf einer einzelnen Architektur kann zu schlechterem Code auf der anderen führen. Ich denke, Sie würden dies entweder für eine bestimmte CPU optimieren oder es so lassen, wie es ist, und den Compiler entscheiden lassen, was er für besser hält.
quelle
Die meisten modernen Architekturen verfügen über Anweisungen zum Ermitteln der Position des niedrigsten gesetzten Bits oder des höchsten gesetzten Bits oder zum Zählen der Anzahl führender Nullen usw.
Wenn Sie eine Anweisung dieser Klasse haben, können Sie die anderen kostengünstig emulieren.
Nehmen Sie sich einen Moment Zeit, um es auf Papier durchzuarbeiten und festzustellen, dass
x & (x-1)
das niedrigste gesetzte Bit in x( x & ~(x-1) )
gelöscht wird und nur das niedrigste gesetzte Bit zurückgegeben wird, unabhängig von Architektur, Wortlänge usw. Wenn Sie dies wissen, ist es trivial, die Hardware-Zählung zu verwenden -zeroes / höchstes gesetztes Bit, um das niedrigste gesetzte Bit zu finden, wenn keine explizite Anweisung dazu vorhanden ist.Wenn überhaupt keine relevante Hardwareunterstützung vorhanden ist, wird die Multiplikations- und Suchimplementierung von Zähl-führenden Nullen angegeben hier oder einer von denen auf der Bit Twiddling Hacks Seite kann trivialerweise zu geben niedrigsten Satz umgewandelt werden Bit die obigen Identitäten und hat den Vorteil, verzweigt zu sein.
quelle
Weee, jede Menge Lösungen und kein Benchmark in Sicht. Ihr Leute solltet euch schämen ;-)
Mein Computer ist ein Intel i530 (2,9 GHz) mit Windows 7 64-Bit. Ich habe mit einer 32-Bit-Version von MinGW kompiliert.
Mein Code:
quelle
BSF
Eine falsche Abhängigkeit von ihrer Ausgabe haben (seit dem tatsächlichen Verhalten) Wenn input = 0 die Ausgabe unverändert lassen soll, verwandelt gcc dies leider in eine schleifenübertragene Abhängigkeit, indem das Register zwischen den Schleifeniterationen nicht gelöscht wird. Daher sollte die Schleife mit einem von 5 Zyklen ausgeführt werden, was einen Engpass bei BSF (3) + CMOV darstellt (2) Latenzffs()
sollte ein Durchsatz von einem pro Takt vorliegen (3 Uops, 1 für BSF und 2 für CMOV, und sie können auf verschiedenen Ports ausgeführt werden). Mit dem gleichen Loop-Overhead können 7 ALU-Uops (auf Ihrer CPU) mit 3 pro Takt ausgeführt werden. Overhead dominiert! Quelle: agner.org/optimizebsf ecx, [ebx+edx*4]
sie nichtecx
als Eingabe behandelt wird, auf die gewartet werden muss. (ECX wurde zuletzt von der CMOV des vorherigen Iteratons geschrieben). Die CPU verhält sich jedoch so, um das Verhalten "Ziel unverändert lassen, wenn Quelle Null ist" zu implementieren (es handelt sich also nicht wirklich um eine falsche Abhängigkeit wie bei TZCNT; eine Datenabhängigkeit ist erforderlich, da unter der Annahme keine Verzweigung + spekulative Ausführung erfolgt dass der Eingang nicht Null ist). Wir könnten es überwinden, indem wir einxor ecx,ecx
vor dem hinzufügenbsf
, um die Abhängigkeit von ECX zu brechen.Die schnellste (nicht intrinsische / nicht Assembler-) Lösung besteht darin, das niedrigste Byte zu finden und dieses Byte dann in einer Nachschlagetabelle mit 256 Einträgen zu verwenden. Dies gibt Ihnen eine Worst-Case-Leistung von vier bedingten Anweisungen und eine Best-Case-Leistung von 1. Dies ist nicht nur die geringste Anzahl von Anweisungen, sondern auch die geringste Anzahl von Verzweigungen, was bei moderner Hardware überaus wichtig ist.
Ihre Tabelle (256 8-Bit-Einträge) sollte den Index des LSB für jede Zahl im Bereich von 0 bis 255 enthalten. Sie überprüfen jedes Byte Ihres Werts und finden das niedrigste Byte ungleich Null. Verwenden Sie diesen Wert dann, um den realen Index zu suchen.
Dies erfordert 256 Byte Speicher, aber wenn die Geschwindigkeit dieser Funktion so wichtig ist, lohnt es sich, 256 Byte zu verwenden.
Z.B
quelle
OMG hat dies gerade gewunden.
Was den meisten dieser Beispiele fehlt, ist ein wenig Verständnis dafür, wie die gesamte Hardware funktioniert.
Jedes Mal, wenn Sie einen Zweig haben, muss die CPU erraten, welcher Zweig verwendet wird. Die Anweisungspipe wird mit den Anweisungen geladen, die den erratenen Pfad hinunterführen. Wenn die CPU falsch geraten hat, wird die Anweisungspipe geleert und der andere Zweig muss geladen werden.
Betrachten Sie die einfache while-Schleife oben. Die Vermutung wird sein, innerhalb der Schleife zu bleiben. Es wird mindestens einmal falsch sein, wenn es die Schleife verlässt. Dadurch wird die Anweisungsleitung gespült. Dieses Verhalten ist etwas besser als die Vermutung, dass es die Schleife verlässt. In diesem Fall würde es die Anweisungspipe bei jeder Iteration leeren.
Die Anzahl der CPU-Zyklen, die verloren gehen, variiert stark von einem Prozessortyp zum nächsten. Sie können jedoch mit 20 bis 150 verlorenen CPU-Zyklen rechnen.
In der nächst schlechteren Gruppe denken Sie, dass Sie einige Iterationen sparen werden, indem Sie den Wert in kleinere Teile aufteilen und mehrere weitere Zweige hinzufügen. Jeder dieser Zweige bietet eine zusätzliche Möglichkeit, die Anweisungsleitung zu spülen, und kostet weitere 20 bis 150 Taktzyklen.
Betrachten wir, was passiert, wenn Sie einen Wert in einer Tabelle nachschlagen. Möglicherweise befindet sich der Wert derzeit nicht im Cache, zumindest nicht beim ersten Aufruf Ihrer Funktion. Dies bedeutet, dass die CPU blockiert wird, während der Wert aus dem Cache geladen wird. Auch dies variiert von Maschine zu Maschine. Die neuen Intel-Chips nutzen dies tatsächlich als Gelegenheit, Threads auszutauschen, während der aktuelle Thread auf den Abschluss des Cache-Ladevorgangs wartet. Dies kann leicht teurer sein als eine Spülung der Anweisungsleitung. Wenn Sie diesen Vorgang jedoch mehrmals ausführen, tritt er wahrscheinlich nur einmal auf.
Die schnellste Lösung mit konstanter Zeit ist eindeutig eine, die deterministische Mathematik beinhaltet. Eine reine und elegante Lösung.
Ich entschuldige mich, wenn dies bereits behandelt wurde.
Jeder von mir verwendete Compiler mit Ausnahme von XCODE AFAIK verfügt über Compiler-Eigenschaften sowohl für den Vorwärts-Bitscan als auch für den Rückwärts-Bitscan. Diese werden auf den meisten Hardwarekomponenten ohne Cache-Miss, ohne Branch-Miss-Vorhersage und ohne andere vom Programmierer generierte Stolpersteine zu einer einzigen Assembly-Anweisung kompiliert.
Verwenden Sie für Microsoft-Compiler _BitScanForward & _BitScanReverse.
Verwenden Sie für GCC __builtin_ffs, __builtin_clz, __builtin_ctz.
Bitte unterlassen Sie es außerdem, eine Antwort zu veröffentlichen und möglicherweise Neulinge irrezuführen, wenn Sie nicht ausreichend über das besprochene Thema informiert sind.
Es tut mir leid, dass ich völlig vergessen habe, eine Lösung bereitzustellen. Dies ist der Code, den ich auf dem IPAD verwende und der keine Anweisung auf Assembly-Ebene für die Aufgabe enthält:
Hier ist zu verstehen, dass nicht der Vergleich teuer ist, sondern der Zweig, der nach dem Vergleich auftritt. Der Vergleich wird in diesem Fall auf einen Wert von 0 oder 1 mit dem Wert .. == 0 gezwungen, und das Ergebnis wird verwendet, um die Mathematik zu kombinieren, die auf beiden Seiten des Zweigs aufgetreten wäre.
Bearbeiten:
Der obige Code ist völlig kaputt. Dieser Code funktioniert und ist immer noch verzweigungsfrei (falls optimiert):
Dies gibt -1 zurück, wenn 0 angegeben wird. Wenn Sie sich nicht für 0 interessieren oder gerne 31 für 0 erhalten, entfernen Sie die i0-Berechnung, um Zeit zu sparen.
quelle
-O3
godbolt.org/z/gcsUHdInspiriert von diesem ähnlichen Beitrag , bei dem nach einem festgelegten Bit gesucht wird, biete ich Folgendes an:
Vorteile:
Nachteile:
Update: Wie in den Kommentaren erwähnt, ist eine Gewerkschaft eine sauberere Implementierung (zumindest für C) und würde folgendermaßen aussehen:
Dies setzt 32-Bit-Ints mit Little-Endian-Speicher für alles voraus (denken Sie an x86-Prozessoren).
quelle
int
istint32_t
, und die unterzeichnete Rechtsverschiebung ist eine arithmetische Verschiebung (in C ++ es der Implementierung definiert)Dies kann mit einem Worst-Case von weniger als 32 Operationen durchgeführt werden:
Prinzip: Überprüfen auf 2 oder mehr Bits ist genauso effizient wie das Überprüfen auf 1 Bit.
Zum Beispiel hindert Sie nichts daran, zuerst zu überprüfen, für welche Gruppierung es sich handelt, und dann jedes Bit vom kleinsten zum größten in dieser Gruppe zu überprüfen.
Also ...
wenn Sie 2 Bits gleichzeitig prüfen, haben Sie im schlimmsten Fall (Nbit / 2) + 1 Schecks insgesamt.
Wenn Sie 3 Bits gleichzeitig prüfen, haben Sie im schlimmsten Fall (Nbit / 3) + 2 Prüfungen insgesamt.
...
Optimal wäre es, Gruppen von 4 Personen einzuchecken. Dies würde im schlimmsten Fall 11 Operationen anstelle Ihrer 32 erfordern.
Der beste Fall reicht von 1 Prüfung Ihrer Algorithmen bis zu 2 Prüfungen, wenn Sie diese Gruppierungsidee verwenden. Aber dieser zusätzliche Scheck im besten Fall lohnt sich für die Einsparungen im schlimmsten Fall.
Hinweis: Ich schreibe es vollständig aus, anstatt eine Schleife zu verwenden, weil es auf diese Weise effizienter ist.
quelle
Warum nicht die binäre Suche verwenden ? Dies wird immer nach 5 Operationen abgeschlossen (unter der Annahme einer int-Größe von 4 Bytes):
quelle
Eine andere Methode (Modulteilung und Suche) verdient hier eine besondere Erwähnung aus demselben Link, der von @ anton-tykhyy bereitgestellt wird. Diese Methode ist in der Leistung der DeBruijn-Multiplikations- und Suchmethode sehr ähnlich, mit einem kleinen, aber wichtigen Unterschied.
Modulteilung und Suche
Die Modulteilungs- und Suchmethode gibt unterschiedliche Werte für v = 0x00000000 und v = FFFFFFFF zurück, während die DeBruijn-Multiplikations- und Suchmethode an beiden Eingängen Null zurückgibt.
Prüfung:-
quelle
mod
ist langsam. Stattdessen können Sie die Original - Multiply-and-Lookup - Methode verwenden und subtrahieren!v
vonr
dem Rande Fälle zu behandeln.Laut der BitScan-Seite zur Schachprogrammierung und meinen eigenen Messungen ist Subtrahieren und Xor schneller als Negieren und Maskieren .
(Beachten Sie, dass, wenn Sie die nachfolgenden Nullen zählen möchten
0
, die Methode, wie ich sie habe, zurückgegeben wird,63
während das Negieren und die Maske zurückgegeben werden0
.)Hier ist eine 64-Bit-Subtraktion und xor:
Als Referenz finden Sie hier eine 64-Bit-Version der Negate- und Mask-Methode:
quelle
(v ^ (v-1))
funktioniert vorausgesetztv != 0
. Im Falle, dassv == 0
es 0xFF .... FF zurückgibt, während(v & -v)
es Null gibt (was übrigens auch falsch ist, buf zumindest führt es zu einem vernünftigen Ergebnis).v ^ (v-1)
, sodass sie nicht voneinander unterschieden werden können. In meinem Szenario wird niemals Null eingegeben.Sie können überprüfen, ob eines der Bits niedrigerer Ordnung gesetzt ist. Wenn ja, dann schauen Sie sich die untere Ordnung der verbleibenden Bits an. z.B,:
32bit int - Überprüfen Sie, ob eine der ersten 16 eingestellt ist. Wenn ja, prüfen Sie, ob eine der ersten 8 eingestellt ist. wenn ja, ....
Wenn nicht, prüfen Sie, ob eine der oberen 16 eingestellt ist.
Im Wesentlichen ist es binäre Suche.
quelle
In meiner Antwort hier erfahren Sie, wie Sie dies mit einem einzelnen x86-Befehl tun. Mit der Ausnahme, dass Sie zum Auffinden des niedrigstwertigen gesetzten Bits den
BSF
Befehl ("Bit Scan Forward") anstelle des dortBSR
beschriebenen verwenden möchten .quelle
Noch eine andere Lösung, möglicherweise nicht die schnellste, scheint aber recht gut zu sein.
Zumindest hat es keine Zweige. ;)
quelle
1
s von der niedrigstwertigen 1 auf LSB zu bringen, verwenden Sie((x & -x) - 1) << 1
stattdessenx ^ (x-1)
50% aller Zahlen werden in der ersten Codezeile zurückgegeben.
75% aller Zahlen werden in den ersten beiden Codezeilen zurückgegeben.
87% aller Zahlen werden in den ersten 3 Codezeilen zurückgegeben.
94% aller Zahlen werden in den ersten 4 Codezeilen zurückgegeben.
97% aller Zahlen werden in den ersten 5 Codezeilen zurückgegeben.
etc.
Ich denke, Leute, die sich darüber beschweren, wie ineffizient das Worst-Case-Szenario für diesen Code ist, verstehen nicht, wie selten dieser Zustand auftreten wird.
quelle
Fand diesen cleveren Trick mit 'magischen Masken' in "Die Kunst des Programmierens, Teil 4", der es in O (log (n)) Zeit für n-Bit-Zahlen macht. [mit log (n) zusätzlichem Speicherplatz]. Typische Lösungen, die nach dem gesetzten Bit suchen, sind entweder O (n) oder benötigen O (n) zusätzlichen Platz für eine Nachschlagetabelle. Dies ist also ein guter Kompromiss.
Magische Masken:
Schlüsselidee: Anzahl der nachgestellten Nullen in x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
quelle
Wenn C ++ 11 für Sie verfügbar ist, kann ein Compiler manchmal die Aufgabe für Sie erledigen :)
Ergebnis ist ein 1-basierter Index.
quelle
ffs()
zur Kompilierungszeit ausgewertet werden, sodass Sie dies nicht verwenden müssen, damit die konstante Weitergabe funktioniert. (Inline-Asm müssen Sie natürlich vermeiden.) Wenn Sie wirklich etwas benötigen, das als C ++ 11 funktioniertconstexpr
, können Sie trotzdem GNU C verwenden__builtin_ffs
.Dies betrifft die Antwort von @Anton Tykhyy
Hier ist meine C ++ 11 constexpr-Implementierung, die Casts beseitigt und eine Warnung in VC ++ 17 entfernt, indem ein 64-Bit-Ergebnis auf 32 Bit gekürzt wird:
Um das Problem zu umgehen, dass 0x1 und 0x0 beide 0 zurückgeben, können Sie Folgendes tun:
Wenn der Compiler den Aufruf jedoch nicht vorverarbeiten kann oder will, werden der Berechnung einige Zyklen hinzugefügt.
Wenn Sie interessiert sind, finden Sie hier eine Liste statischer Asserts, um zu überprüfen, ob der Code das tut, was beabsichtigt ist:
quelle
Hier ist eine einfache Alternative, obwohl das Auffinden von Protokollen etwas kostspielig ist.
quelle
Vor kurzem habe ich gesehen, dass Singapurs Premier ein Programm gepostet hat, das er auf Facebook geschrieben hat. Es gibt eine Zeile, in der es erwähnt wird.
Die Logik ist einfach "Wert & Wert", angenommen, Sie haben 0x0FF0, dann 0FF0 & (F00F + 1), was 0x0010 entspricht, was bedeutet, dass die niedrigste 1 im 4. Bit ist .. :)
quelle
Wenn Sie über die Ressourcen verfügen, können Sie Speicher opfern, um die Geschwindigkeit zu verbessern:
Hinweis: Diese Tabelle würde mindestens 4 GB verbrauchen (16 GB, wenn wir den Rückgabetyp als belassen
unsigned
). Dies ist ein Beispiel für den Handel einer begrenzten Ressource (RAM) gegen eine andere (Ausführungsgeschwindigkeit).Wenn Ihre Funktion portabel bleiben und um jeden Preis so schnell wie möglich ausgeführt werden muss, ist dies der richtige Weg. In den meisten realen Anwendungen ist eine 4-GB-Tabelle unrealistisch.
quelle
:)
@Dan: Sie haben Recht mit dem Zwischenspeichern von Speicher. Siehe den Kommentar von Mikeage oben.