Wenn ich eine ganze Zahl n habe und die Position des höchstwertigen Bits wissen möchte (dh wenn das niedrigstwertige Bit rechts ist, möchte ich die Position des am weitesten links liegenden Bits wissen, das eine 1 ist). Was ist die schnellste / effizienteste Methode, um dies herauszufinden?
Ich weiß, dass POSIX eine ffs()
Methode in strings.h unterstützt, um das erste gesetzte Bit zu finden, aber es scheint keine entsprechende fls()
Methode zu geben.
Gibt es einen wirklich offensichtlichen Weg, dies zu tun, den ich vermisse?
Was ist in Fällen, in denen Sie POSIX-Funktionen für die Portabilität nicht verwenden können?
Bearbeiten: Was ist mit einer Lösung, die sowohl auf 32- als auch auf 64-Bit-Architekturen funktioniert (viele der Codelisten scheinen nur auf 32-Bit-Ints zu funktionieren).
Antworten:
GCC hat :
Ich würde erwarten, dass sie in etwas übersetzt werden, das für Ihre aktuelle Plattform einigermaßen effizient ist, sei es einer dieser ausgefallenen Bit-Twiddling-Algorithmen oder eine einzelne Anweisung.
Ein nützlicher Trick, wenn Ihre Eingabe Null sein kann , ist
__builtin_clz(x | 1)
: Das bedingungslose Setzen des Low-Bits, ohne andere zu ändern, bewirkt die Ausgabe31
fürx=0
, ohne die Ausgabe für einen anderen Eingang zu ändern.Um dies zu vermeiden, sind plattformspezifische Eigenschaften wie ARM-GCCs
__clz
(kein Header erforderlich) oder x86-_lzcnt_u32
CPUs auf CPUs, die dielzcnt
Anweisung unterstützen , eine andere Option . (Vorsicht, daslzcnt
dekodiert alsbsr
Beachten bei älteren CPUs erfolgt, anstatt Fehler zu verursachen, was 31-lzcnt für Eingaben ungleich Null ergibt.)Es gibt leider keine Möglichkeit, die verschiedenen CLZ-Anweisungen auf Nicht-x86-Plattformen, die das Ergebnis für input = 0 als 32 oder 64 definieren (je nach Operandenbreite), portabel zu nutzen. x86
lzcnt
macht das auch, während esbsr
einen Bitindex erzeugt, den der Compiler umdrehen muss, wenn Sie ihn nicht verwenden31-__builtin_clz(x)
.(Das "undefinierte Ergebnis" ist nicht C Undefiniertes Verhalten, sondern nur ein Wert, der nicht definiert ist. Es ist eigentlich alles, was sich im Zielregister befand, als die Anweisung ausgeführt wurde. AMD dokumentiert dies, Intel nicht, aber Intels CPUs implementieren dieses Verhalten Aber es ist nicht das, was zuvor in der C-Variablen war, der Sie zugewiesen haben. So funktioniert es normalerweise nicht, wenn gcc C in asm umwandelt . Siehe auch Warum ist es wichtig, die "Ausgabeabhängigkeit" von LZCNT zu brechen? )
quelle
__builtin_ctz
overffs
, das zu einem BSF und einem CMOV kompiliert wird, um den Fall von Eingabe-war-Null zu behandeln. Auf Architekturen ohne ausreichend kurze Implementierung (z. B. altes ARM ohneclz
Anweisung) gibt gcc einen Aufruf einer libgcc-Hilfsfunktion aus.Angenommen, Sie sind auf x86 und spielen ein bisschen Inline-Assembler. Intel bietet eine
BSR
Anweisung ("Bit Scan Reverse"). Es ist schnell auf einigen x86s (Mikrocode auf andere). Aus dem Handbuch:(Wenn Sie auf PowerPC sind, gibt es eine ähnliche
cntlz
Anweisung ("führende Nullen zählen").)Beispielcode für gcc:
Siehe auch dieses Inline-Assembler-Tutorial , das zeigt (Abschnitt 9.4), dass es erheblich schneller ist als das Schleifen von Code.
quelle
Da 2 ^ N eine ganze Zahl ist, bei der nur das N-te Bit gesetzt ist (1 << N), ist das Finden der Position (N) des höchsten gesetzten Bits die ganzzahlige Protokollbasis 2 dieser ganzen Zahl.
http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious
Dieser "offensichtliche" Algorithmus ist möglicherweise nicht für alle transparent, aber wenn Sie feststellen, dass sich der Code wiederholt um ein Bit nach rechts verschiebt, bis das Bit ganz links verschoben wurde (beachten Sie, dass C jeden Wert ungleich Null als wahr behandelt) und die Zahl zurückgibt von Schichten macht es vollkommen Sinn. Dies bedeutet auch, dass es auch dann funktioniert, wenn mehr als ein Bit gesetzt ist - das Ergebnis ist immer für das höchstwertige Bit.
Wenn Sie auf dieser Seite nach unten scrollen, gibt es schnellere und komplexere Variationen. Wenn Sie jedoch wissen, dass Sie mit Zahlen mit vielen führenden Nullen arbeiten, bietet der naive Ansatz möglicherweise eine akzeptable Geschwindigkeit, da die Bitverschiebung in C ziemlich schnell ist und der einfache Algorithmus keine Indizierung eines Arrays erfordert.
HINWEIS: Seien Sie bei der Verwendung von 64-Bit-Werten äußerst vorsichtig, wenn Sie besonders clevere Algorithmen verwenden. Viele von ihnen funktionieren nur für 32-Bit-Werte korrekt.
quelle
>>>
. Plus wahrscheinlich der Komparator!= 0
und eine nicht spezifizierte Anzahl von Klammern.Dies sollte blitzschnell sein:
quelle
Dies ist so etwas wie das Finden einer Art Ganzzahlprotokoll. Es gibt ein bisschen Tricks, aber ich habe mein eigenes Werkzeug dafür gemacht. Das Ziel ist natürlich die Geschwindigkeit.
Meine Erkenntnis ist, dass die CPU bereits einen automatischen Bitdetektor hat, der für die Ganzzahl-Float-Konvertierung verwendet wird! Also benutze das.
Diese Version wandelt den Wert in ein Double um und liest dann den Exponenten ab, der Ihnen sagt, wo sich das Bit befand. Die ausgefallene Verschiebung und Subtraktion besteht darin, die richtigen Teile aus dem IEEE-Wert zu extrahieren.
Die Verwendung von Floats ist etwas schneller, aber ein Float kann Ihnen aufgrund seiner geringeren Genauigkeit nur die ersten 24-Bit-Positionen geben.
Um dies sicher und ohne undefiniertes Verhalten in C ++ oder C zu tun, verwenden Sie
memcpy
anstelle des Zeiger-Castings das Typ-Punning. Compiler wissen, wie man es effizient einbindet.Oder verwenden Sie in C99 und höher a
union {double d; uint32_t u[2];};
. Beachten Sie jedoch, dass in C ++ das Punnen vom Unionstyp nur auf einigen Compilern als Erweiterung unterstützt wird, nicht in ISO C ++.Dies ist normalerweise langsamer als eine plattformspezifische Eigenschaft für einen Zählbefehl mit führenden Nullen, aber tragbares ISO C hat keine solche Funktion. Einige CPUs haben auch keinen Befehl zum Zählen von führenden Nullen, aber einige von diesen können Ganzzahlen effizient in konvertieren
double
. Das Zurückschreiben eines FP-Bitmusters auf eine Ganzzahl kann jedoch langsam sein (z. B. erfordert es auf PowerPC ein Speichern / Neuladen und verursacht normalerweise ein Laden-Hit-Store-Stillstand).Dieser Algorithmus könnte möglicherweise für SIMD-Implementierungen nützlich sein, da weniger CPUs über SIMD verfügen
lzcnt
. x86 hat eine solche Anweisung nur mit AVX512CD erhaltenquelle
Kaz Kylheku hier
Ich habe zwei Ansätze für diese über 63-Bit-Zahlen (den langen langen Typ auf gcc x86_64) verglichen, wobei ich mich vom Vorzeichenbit fernhielt.
(Ich brauche zufällig dieses "höchste Bit finden" für etwas, verstehen Sie?)
Ich habe die datengesteuerte binäre Suche implementiert (eng basierend auf einer der obigen Antworten). Ich habe auch einen vollständig abgewickelten Entscheidungsbaum von Hand implementiert, der nur Code mit unmittelbaren Operanden ist. Keine Schleifen, keine Tabellen.
Der Entscheidungsbaum (höchstes_bit_unrolled) wurde mit 69% schneller bewertet, mit Ausnahme des Falls n = 0, für den die binäre Suche einen expliziten Test hat.
Der Spezialtest der Binärsuche für den Fall 0 ist nur 48% schneller als der Entscheidungsbaum, für den es keinen Spezialtest gibt.
Compiler, Maschine: (GCC 4.5.2, -O3, x86-64, 2867 MHz Intel Core i5).
Schnelles und schmutziges Testprogramm:
Wenn nur -O2 verwendet wird, wird der Unterschied größer. Der Entscheidungsbaum ist fast viermal schneller.
Ich habe mich auch mit dem naiven Bitverschiebungscode verglichen:
Dies ist nur für kleine Zahlen schnell, wie man erwarten würde. Bei der Bestimmung, dass das höchste Bit 1 für n == 1 ist, wurde ein Benchmarking von mehr als 80% schneller durchgeführt. Bei der Hälfte der zufällig ausgewählten Zahlen im 63-Bit-Raum ist jedoch das 63. Bit gesetzt!
Bei der Eingabe 0x3FFFFFFFFFFFFFFF ist die Entscheidungsbaumversion ziemlich viel schneller als bei 1 und zeigt sich als 1120% schneller (12,2-mal) als der Bit-Shifter.
Ich werde den Entscheidungsbaum auch mit den GCC-Buildins vergleichen und auch eine Mischung von Eingaben versuchen, anstatt sie mit derselben Zahl zu wiederholen. Möglicherweise gibt es eine Vorhersage für bleibende Zweige und möglicherweise einige unrealistische Caching-Szenarien, die die Wiederholung künstlich beschleunigen.
quelle
Wie wäre es mit
?
quelle
1 Register, 13 Anweisungen. Ob Sie es glauben oder nicht, dies ist normalerweise schneller als der oben erwähnte BSR-Befehl, der in linearer Zeit arbeitet. Dies ist die logarithmische Zeit.
Von http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit
quelle
__builtin_clz
wenn es mit-march=native
oder etwas aktiviert ist (da es auf jeder CPU, die es unterstützt, schnell ist). Selbst auf CPUs wie der AMD Bulldozer-Familie, bei denen BSR "langsam" ist, ist es nicht so langsam: 7 M-Ops mit 4-Zyklus-Latenz und einer pro 4c-Durchsatz. Auf Atom ist BSR sehr langsam: 16 Zyklen. Auf Silvermont sind es 10 Uops mit einer Latenz von 10 Zyklen. Dies könnte eine etwas geringere Latenz als BSR auf Silvermont sein, aber IDK.Hier sind einige (einfache) Benchmarks der derzeit auf dieser Seite angegebenen Algorithmen ...
Die Algorithmen wurden nicht für alle Eingaben von int ohne Vorzeichen getestet. also überprüfe das zuerst, bevor du blind etwas benutzt;)
Auf meinem Computer funktionieren clz (__builtin_clz) und asm am besten. asm scheint noch schneller als clz ... aber es könnte an dem einfachen Benchmark liegen ...
quelle
Obwohl ich diese Methode wahrscheinlich nur verwenden würde, wenn ich unbedingt die bestmögliche Leistung benötigen würde (z. B. um eine Art Brettspiel-KI mit Bitboards zu schreiben), ist die effizienteste Lösung die Verwendung von Inline-ASM. Im Abschnitt Optimierungen dieses Blogposts finden Sie Code mit einer Erklärung.
quelle
Ich brauchte eine Routine, um dies zu tun, und bevor ich das Web durchsuchte (und diese Seite fand), fand ich meine eigene Lösung, die auf einer binären Suche basierte. Obwohl ich sicher bin, dass jemand dies schon einmal getan hat! Es läuft in konstanter Zeit und kann schneller sein als die "offensichtliche" Lösung, obwohl ich keine großen Ansprüche stelle, sondern es nur aus Interesse poste.
quelle
Das ist eine Art binäre Suche, die mit allen Arten von (vorzeichenlosen!) Ganzzahltypen funktioniert
zu vervollständigen:
quelle
typedef
s oder irgendetwas anderes als Präprozessor-Makros zu verwenden. Dies ist eine weithin akzeptierte Konvention.Einige zu komplexe Antworten hier. Die Debruin-Technik sollte nur verwendet werden, wenn der Eingang bereits eine Zweierpotenz ist, andernfalls gibt es einen besseren Weg. Bei einer Leistung von 2 Eingängen ist Debruin der absolut schnellste, sogar schneller als
_BitScanReverse
auf jedem von mir getesteten Prozessor. Im allgemeinen Fall jedoch_BitScanReverse
(oder wie auch immer das Intrinsic in Ihrem Compiler heißt) ist es jedoch am schnellsten (auf bestimmten CPUs kann es jedoch mikrocodiert werden).Wenn die intrinsische Funktion keine Option ist, finden Sie hier eine optimale Softwarelösung für die Verarbeitung allgemeiner Eingaben.
Beachten Sie, dass diese Version im Gegensatz zu den meisten anderen Antworten am Ende keine Debruin-Suche erfordert. Es berechnet die Position an Ort und Stelle.
Tabellen können jedoch vorzuziehen sein, wenn Sie sie wiederholt genug aufrufen, wird das Risiko eines Cache-Fehlers durch die Beschleunigung einer Tabelle verdunkelt.
Dies sollte den höchsten Durchsatz aller hier angegebenen Softwareantworten liefern. Wenn Sie ihn jedoch nur gelegentlich aufrufen, bevorzugen Sie eine tabellenfreie Lösung wie mein erstes Snippet.
quelle
Wie die obigen Antworten zeigen, gibt es eine Reihe von Möglichkeiten, das höchstwertige Bit zu bestimmen. Wie bereits erwähnt, sind die Methoden jedoch wahrscheinlich nur für 32-Bit- oder 64-Bit-Register eindeutig. Die Seite stanford.edu bithacks bietet Lösungen, die sowohl für 32- Bit- als auch für 64-Bit-Computer geeignet sind . Mit ein wenig Arbeit können sie kombiniert werden, um einen soliden architekturübergreifenden Ansatz für den Erhalt des MSB bereitzustellen. Die Lösung, die ich bei der Kompilierung / Arbeit auf 64- und 32-Bit-Computern gefunden habe, war:
quelle
#ifdef BUILD_64
Flagge definiert ? In diesem Fall wäre eine Neudefinition innerhalb der Bedingung nicht erforderlich.Eine Version in C mit sukzessiver Approximation:
Vorteil: Die Laufzeit ist unabhängig von der angegebenen Anzahl konstant, da die Anzahl der Schleifen immer gleich ist. (4 Schleifen bei Verwendung von "unsigned int")
quelle
msb += (n>>msb) ? step : -step;
) schreiben , werden wahrscheinlich mehr Compiler verzweigungslose Asm erstellen , um Fehlvorhersagen für Verzweigungen bei jedem Schritt zu vermeiden ( stackoverflow.com/questions/11227809/… ).Ich weiß, dass diese Frage sehr alt ist, aber nachdem ich selbst eine msb () -Funktion implementiert habe, stellte ich fest, dass die meisten hier und auf anderen Websites vorgestellten Lösungen nicht unbedingt die effizientesten sind - zumindest für meine persönliche Definition von Effizienz (siehe auch Update unten) ). Hier ist der Grund:
Die meisten Lösungen (insbesondere diejenigen, die ein binäres Suchschema oder den naiven Ansatz verwenden, bei dem ein linearer Scan von rechts nach links durchgeführt wird) scheinen die Tatsache zu vernachlässigen, dass es für beliebige binäre Zahlen nicht viele gibt, die mit einer sehr langen Folge von beginnen Nullen. Tatsächlich beginnt für jede Bitbreite die Hälfte aller Ganzzahlen mit einer 1 und ein Viertel von ihnen mit 01 . Sehen Sie, wo ich hinkomme? Mein Argument ist, dass ein linearer Scan von der höchstwertigen Bitposition bis zur niedrigstwertigen (von links nach rechts) nicht so "linear" ist, wie es auf den ersten Blick aussehen könnte.
Es kann 1 gezeigt werden , dass für jede Bitbreite die durchschnittliche Anzahl von Bits, die getestet werden müssen, höchstens 2 beträgt. Dies führt zu einer amortisierten Zeitkomplexität von O (1) in Bezug auf die Anzahl von Bits (!). .
Natürlich ist der schlimmste Fall immer noch O (n) , schlimmer als der O (log (n)), den Sie bei binärsuchähnlichen Ansätzen erhalten, aber da es so wenige schlimmste Fälle gibt, sind sie für die meisten Anwendungen vernachlässigbar ( Update) : nicht ganz: Es mag wenige geben, aber sie können mit hoher Wahrscheinlichkeit auftreten - siehe Update unten).
Hier ist der "naive" Ansatz, den ich mir ausgedacht habe und der zumindest auf meinem Computer die meisten anderen Ansätze übertrifft (binäre Suchschemata für 32-Bit-Ints erfordern immer log 2 (32) = 5 Schritte, während dieser alberne Algorithmus weniger erfordert als durchschnittlich 2) - Entschuldigung, dass dies C ++ und nicht reines C ist:
Update : Während das, was ich hier geschrieben habe, für beliebige Ganzzahlenvollkommen zutrifft, bei denen jede Kombination von Bits gleich wahrscheinlich ist (mein Geschwindigkeitstest hat einfach gemessen, wie lange es gedauert hat, das MSB für alle 32-Bit-Ganzzahlenzu bestimmen), für reale Ganzzahlen, z Welche solche Funktion aufgerufen wird, folgt normalerweise einem anderen Muster: In meinem Code wird diese Funktion beispielsweise verwendet, um zu bestimmen, ob eine Objektgröße eine Potenz von 2 ist, oder um die nächste Potenz von 2 größer oder gleich einer zu finden Objektgröße . Ich vermute, dass die meisten Anwendungen, die das MSB verwenden, Zahlen enthalten, die viel kleiner sind als die maximale Zahl, die eine Ganzzahl darstellen kann (Objektgrößen verwenden selten alle Bits in einem size_t). In diesem Fall ist meine Lösung tatsächlich schlechter als ein binärer Suchansatz. Letzterer sollte daher wahrscheinlich bevorzugt werden, obwohl meine Lösung alle Ganzzahlen schneller durchläuft .
TL; DR: Reale Ganzzahlen werden wahrscheinlich eine Tendenz zum schlimmsten Fall dieses einfachen Algorithmus haben, was die Leistung am Ende verschlechtern wird - trotz der Tatsache, dass O (1) für wirklich beliebige Ganzzahlen amortisiert ist .
1 Das Argument lautet wie folgt (grober Entwurf): Sei n die Anzahl der Bits (Bitbreite). Es gibt insgesamt 2 n ganze Zahlen, die mit n Bits dargestellt werden können. Es gibt 2 n - 1 Ganzzahlen, die mit einer 1 beginnen (die erste 1 ist fest, die verbleibenden n - 1 Bits können alles sein). Diese ganzen Zahlen erfordern nur eine Interaktion der Schleife, um das MSB zu bestimmen. Ferner gibt es 2 n - 2 Ganzzahlen, die mit 01 beginnen und 2 Iterationen erfordern, 2 n - 3 Ganzzahlen, die mit 001 beginnen , 3 Iterationen erfordern und so weiter.
Wenn wir alle erforderlichen Iterationen für alle möglichen Ganzzahlen zusammenfassen und durch 2 n , die Gesamtzahl der Ganzzahlen, dividieren , erhalten wir die durchschnittliche Anzahl der Iterationen, die zur Bestimmung des MSB für n- Bit-Ganzzahlen erforderlich sind:
(1 * 2 n - 1 + 2 * 2 n - 2 + 3 * 2 n - 3 + ... + n) / 2 n
Diese Reihe von durchschnittlichen Iterationen ist tatsächlich konvergent und hat eine Grenze von 2 für n gegen unendlich
Somit hat der naive Links-Rechts-Algorithmus tatsächlich eine amortisierte konstante Zeitkomplexität von O (1) für eine beliebige Anzahl von Bits.
quelle
c99hat uns gegeben
log2
. Dadurch entfallen alle speziellen Saucenimplementierungen,log2
die Sie auf dieser Seite sehen. Sie können dielog2
Implementierung des Standards folgendermaßen verwenden :Ein
n
von0UL
muss auch geschützt werden, weil:Ich habe ein Beispiel mit diesem Scheck geschrieben , dass willkürlich Sätze hier: https://ideone.com/u26vsi
Index
ULONG_MAX
Das Visual-StudioDie Folge von Ephemients gcc ist die einzige Antwort :
Die Dokumentation für
_BitScanReverse
ZuständeIndex
lautet:In der Praxis habe ich festgestellt , dass , wenn
n
ist ,0UL
dassIndex
festgelegt ist0UL
genauso wie es für eine wäre,n
von1UL
. Aber das einzige in der Dokumentation garantiert , was im Fall einesn
von0UL
ist , dass die Rückkehr ist:Ähnlich wie bei der oben beschriebenen bevorzugten
log2
Implementierung sollte daher die Rückgabe überprüft werden, indemIndex
in diesem Fall ein markierter Wert festgelegt wird. Ich habe hier noch einmal ein Beispiel für die VerwendungULONG_MAX
dieses Flag-Werts geschrieben: http://rextester.com/GCU61409quelle
_BitScanReverse
gibt nur dann 0 zurück , wenn die Eingabe war0
. Dies ist wie dieBSR
Anweisung von x86 , mit der ZF nur basierend auf der Eingabe und nicht auf der Ausgabe festgelegt wird. Interessant, dass MS die Dokumente alsindex
nicht gesetzt bezeichnet, wenn kein1
Bit gefunden wird; das entspricht auch dem x86 asm verhalten vonbsr
. (AMD dokumentiert, dass das Zielregister bei src = 0 unverändert bleibt, aber Intel sagt nur undefinierte Ausgabe, obwohl ihre CPUs das unveränderte Verhalten implementieren.) Dies ist anders als bei x86lzcnt
, was32
für nicht gefunden gilt._BitScanReverse
verwendet eine auf Null basierende Indizierung. Wennn
also 1 ist, ist der Index des gesetzten Bits tatsächlich 0. Leidern
ist die Ausgabe , wie Sie sagen, wenn 0 ist, ebenfalls 0 :( Dies bedeutet, dass es keine Möglichkeit gibt, die Rückkehr zu zu verwenden Unterscheide zwischenn
1 und 0. Das habe ich versucht zu kommunizieren. Glaubst du, es gibt einen besseren Weg, dies zu sagen?Index
. Das ist nicht der Rückgabewert . Es wird ein Boolescher Wert zurückgegeben, der falsch ist, wenn die Eingabe Null war (und aus diesem Grund wird der Index als Referenz übergeben, anstatt normal zurückgegeben zu werden). godbolt.org/g/gQKJdE . Und ich habe nachgesehen: Trotz des Wortlauts der MS-Dokumente_BitScanReverse
bleibt der Index nicht deaktiviertn==0
: Sie erhalten nur den Wert in dem Register, das er gerade verwendet hat. (Was in Ihrem Fall wahrscheinlich das gleiche Register war, für das esIndex
später verwendet wurde, was dazu führte, dass Sie ein sehen0
).log2
seit C99.Denken Sie an bitweise Operatoren.
Ich habe die Frage beim ersten Mal falsch verstanden. Sie sollten ein int mit dem am weitesten links stehenden Bit erzeugen (die anderen Nullen). Angenommen, cmp ist auf diesen Wert eingestellt:
quelle
8
sollte seinCHAR_BIT
. Es ist sehr unwahrscheinlich, dass dies der schnellste Weg ist, da beim Verlassen der Schleife eine Verzweigungsfehlvorhersage auftritt, sofern diese nicht wiederholt mit derselben Eingabe verwendet wird. Auch für kleine Eingaben (viele Nullen) muss es eine Menge Schleifen geben. Dies ist wie die Fallback-Methode, die Sie als einfach zu überprüfende Version in einem Komponententest verwenden würden, um sie mit optimierten Versionen zu vergleichen.Wenn man Joshs Benchmark erweitert, kann man das clz wie folgt verbessern
Zum asm: Beachten Sie, dass es bsr und bsrl gibt (dies ist die "lange" Version). das normale könnte etwas schneller sein.
quelle
Beachten Sie, dass Sie versuchen, die Ganzzahl log2 einer Ganzzahl zu berechnen.
Beachten Sie, dass Sie versuchen können, mehr als 1 Bit gleichzeitig zu suchen.
Dieser Ansatz verwendet eine binäre Suche
Eine andere binäre Suchmethode, vielleicht besser lesbar,
Und weil Sie diese testen möchten,
quelle
Dies einzufügen, da es sich um einen „weiteren“ Ansatz handelt, scheint sich von den bereits gegebenen zu unterscheiden.
Gibt
-1
ifx==0
andernfalls zurückfloor( log2(x))
(maximales Ergebnis 31)Reduzieren Sie das 32- auf 4-Bit-Problem und verwenden Sie dann eine Tabelle. Vielleicht unelegant, aber pragmatisch.
Dies ist, was ich verwende, wenn ich es
__builtin_clz
aufgrund von Portabilitätsproblemen nicht verwenden möchte .Um es kompakter zu machen, könnte man stattdessen eine Schleife zum Reduzieren verwenden und jedes Mal 4 zu r hinzufügen, maximal 7 Iterationen. Oder ein Hybrid wie (für 64 Bit): Schleife, um auf 8 zu reduzieren, Test, um auf 4 zu reduzieren.
quelle
Woaw, das waren viele Antworten. Es tut mir nicht leid, eine alte Frage beantwortet zu haben.
Diese Antwort ist einer anderen Antwort ziemlich ähnlich ... na ja.
quelle
1<<k
ist eine nette Geste. Was ist mit den Masken?(1 << (1<<k-1)-1<< (1<<k-1)
? (most optimal
? Sie vergleichen einen Superlativ?)&
und verwendet werden&~
.) Sie können die Hex-Konstanten durch solche ersetzen((type)1<<(1<<k))-1<<(1<<k)
.Der Code:
Oder rufen Sie den ganzzahligen Teil des FPU-Befehls FYL2X (Y * Log2 X) ab, indem Sie Y = 1 setzen
quelle
double
, was wahrscheinlich gut ist, wenn es tatsächlich speichert / neu lädt, anstatt Typ-Wortspiel auf eine andere Weise, z mit einermovq
Anweisung wie Sie könnten hier auf x86 bekommen.[7FFFFFFFFFFFFE00 - 7FFFFFFFFFFFFFFF]
.Ein anderes Poster lieferte eine Nachschlagetabelle mit einer byteweiten Nachschlagetabelle. Für den Fall, dass Sie etwas mehr Leistung erzielen möchten (auf Kosten von 32 KB Speicher anstelle von nur 256 Nachschlageinträgen), finden Sie hier eine Lösung mit einer 15-Bit-Nachschlagetabelle in C # 7 für .NET .
Der interessante Teil ist die Initialisierung der Tabelle. Da es sich um einen relativ kleinen Block handelt, den wir für die Lebensdauer des Prozesses benötigen, ordne ich dafür nicht verwalteten Speicher zu
Marshal.AllocHGlobal
. Wie Sie sehen können, ist das gesamte Beispiel für maximale Leistung als nativ geschrieben:Die Tabelle erfordert eine einmalige Initialisierung über den obigen Code. Es ist schreibgeschützt, sodass eine einzelne globale Kopie für den gleichzeitigen Zugriff freigegeben werden kann. Mit dieser Tabelle können Sie sehen schnell die ganze Zahl log 2 , das istwas wir hier suchen, für alle die verschiedenen ganzzahligen Breiten (8, 16, 32 und 64 Bit).
Beachten Sie, dass der Tabelleneintrag für
0
, die einzige Ganzzahl, für die der Begriff 'höchstes gesetztes Bit' undefiniert ist, den Wert erhält-1
. Diese Unterscheidung ist für die ordnungsgemäße Behandlung von 0-wertigen oberen Wörtern im folgenden Code erforderlich. Hier ist ohne weiteres der Code für jedes der verschiedenen ganzzahligen Grundelemente:ulong (64-Bit) Version
uint (32-Bit) Version
Verschiedene Überladungen für die oben genannten
Dies ist eine vollständige, funktionierende Lösung, die die beste Leistung unter .NET 4.7.2 für zahlreiche Alternativen darstellt, die ich mit einem speziellen Leistungstest-Kabelbaum verglichen habe. Einige davon sind unten aufgeführt. Die Testparameter waren eine gleichmäßige Dichte aller 65-Bit-Positionen, dh 0 ... 31/63 plus Wert
0
(was das Ergebnis -1 ergibt). Die Bits unterhalb der Zielindexposition wurden zufällig gefüllt. Die Tests waren nur x64 , Release-Modus, mit aktivierten JIT-Optimierungen.Das ist das Ende meiner formellen Antwort hier; Im Folgenden finden Sie einige gelegentliche Hinweise und Links zum Quellcode für alternative Testkandidaten, die mit den von mir durchgeführten Tests verknüpft sind, um die Leistung und Richtigkeit des obigen Codes zu überprüfen.
Die oben bereitgestellte Version, die als Tab16A codiert wurde, war über viele Läufe ein konstanter Gewinner. Diese verschiedenen Kandidaten in aktiver Arbeits- / Arbeitsform finden Sie hier , hier und hier .
Bemerkenswert ist, dass die schreckliche Leistung von
ntdll.dll!RtlFindMostSignificantBit
via P / Invoke:Es ist wirklich schade, denn hier ist die gesamte eigentliche Funktion:
Ich kann mir nicht vorstellen, dass die schlechte Leistung von diesen fünf Zeilen herrührt, daher müssen die Strafen für den verwalteten / nativen Übergang schuld sein. Ich war auch überrascht, dass die Tests die
short
direkten Nachschlagetabellen mit 32 KB (und 64 KB) (16 Bit) gegenüber den Nachschlagetabellen mit 128 Byte (und 256 Byte)byte
(8 Bit) wirklich bevorzugten . Ich dachte, das Folgende wäre mit den 16-Bit-Lookups wettbewerbsfähiger, aber letztere übertrafen dies durchweg:Das Letzte, worauf ich hinweisen werde, ist, dass ich ziemlich schockiert war, dass meine deBruijn-Methode nicht besser abgeschnitten hat. Dies ist die Methode, die ich zuvor allgegenwärtig angewendet hatte:
Es gibt viele Diskussionen darüber, wie überlegen und großartig deBruijn-Methoden bei dieser SO-Frage sind , und ich war eher damit einverstanden. Meine Spekulation ist, dass, während sowohl die deBruijn- als auch die Direct-Lookup-Tabellenmethode (die ich als am schnellsten empfunden habe) beide eine Tabellensuche durchführen müssen und beide eine sehr minimale Verzweigung aufweisen, nur der deBruijn eine 64-Bit-Multiplikationsoperation hat. Ich habe nur die
IndexOfMSB
Funktionen hier getestet - nicht das deBruijn -IndexOfLSB
aber ich erwarte, dass letzteres eine viel bessere Chance bietet, da es so viel weniger Operationen hat (siehe oben), und ich werde es wahrscheinlich weiterhin für LSB verwenden.quelle
Meine bescheidene Methode ist sehr einfach:
MSB (x) = INT [Protokoll (x) / Protokoll (2)]
Übersetzung: Das MSB von x ist der ganzzahlige Wert von (Protokoll der Basis x geteilt durch das Protokoll der Basis 2).
Dies kann einfach und schnell an jede Programmiersprache angepasst werden. Probieren Sie es auf Ihrem Taschenrechner aus, um selbst zu sehen, dass es funktioniert.
quelle
int(math.log((1 << 48) - 1) / math.log(2))
48.Hier ist eine schnelle Lösung für C , die in GCC und Clang funktioniert . bereit zum Kopieren und Einfügen.
Und eine etwas verbesserte Version für C ++ .
Der Code geht davon aus, dass dies
value
nicht der Fall ist0
. Wenn Sie 0 zulassen möchten, müssen Sie diese ändern.quelle
Ich gehe davon aus, dass Ihre Frage eine Ganzzahl (unten v genannt) und keine Ganzzahl ohne Vorzeichen ist.
Wenn Sie möchten, dass es funktioniert, ohne das Vorzeichen zu berücksichtigen, können Sie ein zusätzliches 'v << = 1;' vor der Schleife (und ändern Sie den r-Wert entsprechend auf 30). Bitte lassen Sie mich wissen, wenn ich etwas vergessen habe. Ich habe es nicht getestet, aber es sollte gut funktionieren.
quelle
v <<= 1
ist undefiniertes Verhalten (UB) wennv < 0
.0x8000000
, vielleicht meinst du dort eine zusätzliche 0.