Gibt es einen Vorteil bei der Bitmanipulation im C-Stil gegenüber std :: bitset?

15

Ich arbeite fast ausschließlich in C ++ 11/14 und erschaudere normalerweise, wenn ich Code wie diesen sehe:

std::int64_t mArray;
mArray |= someMask << 1;

Dies ist nur ein Beispiel. Ich spreche von bitweiser Manipulation im Allgemeinen. Gibt es in C ++ wirklich irgendeinen Grund? Das oben Genannte ist irritierend und fehleranfällig, während Sie mit a std::bitsetFolgendes tun können:

  1. Sie können die Größe der std::bitsetVorlage nach Bedarf einfacher ändern, indem Sie einen Vorlagenparameter anpassen und die Implementierung den Rest erledigen lassen
  2. Verbringen Sie weniger Zeit damit, herauszufinden, was los ist (und möglicherweise Fehler zu machen), und schreiben Sie std::bitsetin ähnlicher Weise wie in std::arrayanderen Datencontainern.

Meine Frage ist; gibt es keinen Grund nicht zu verwenden std::bitset, nicht zur Rückwärtskompatibilität über primitive Typen?

quant
quelle
Die Größe von a std::bitsetwird zur Kompilierungszeit festgelegt. Das ist der einzige Nachteil, den ich mir vorstellen kann.
rwong
1
@rwong Ich spreche von einer Bitmanipulation im std::bitsetVs-C-Stil (z. B. int), die auch zur Kompilierungszeit behoben wird.
Quant
Ein Grund könnte ein älterer Code sein: Der Code wurde geschrieben, als er std::bitsetnicht verfügbar war (oder dem Autor bekannt war), und es gab keinen Grund, den zu verwendenden Code umzuschreiben std::bitset.
Bart van Ingen Schenau
Ich persönlich bin der Meinung, dass die Frage, wie "Operationen an einer Menge / Map / Array von Binärvariablen" für jedermann leicht verständlich gemacht werden können, noch weitgehend ungelöst ist, da es in der Praxis viele Operationen gibt, die sich nicht auf einfache Operationen reduzieren lassen. Es gibt auch zu viele Möglichkeiten, solche Mengen darzustellen, von denen bitseteine ist, aber ein kleiner Vektor oder eine Menge von ints (von Bitindex) könnte auch legitim sein. Die Philosophie von C / C ++ verbirgt diese Auswahlkomplexitäten nicht vor dem Programmierer.
rwong

Antworten:

12

Aus logischer (nicht technischer) Sicht gibt es keinen Vorteil.

Jeder einfache C / C ++ - Code kann in ein geeignetes "Bibliothekskonstrukt" eingeschlossen werden. Nach einer solchen Umhüllung wird die Frage, ob dies vorteilhafter als das ist, zur strittigen Frage.

Unter dem Gesichtspunkt der Geschwindigkeit sollte C / C ++ es dem Bibliothekskonstrukt ermöglichen, Code zu generieren, der genauso effizient ist wie der einfache Code, den es umschließt. Dies unterliegt jedoch:

  • Funktions-Inlining
  • Überprüfung zur Kompilierungszeit und Vermeidung unnötiger Laufzeitüberprüfungen
  • Beseitigung toter Codes
  • Viele andere Code-Optimierungen ...

Mit dieser Art von nichttechnischem Argument können "fehlende Funktionen" von jedermann hinzugefügt werden und werden daher nicht als Nachteil gewertet.

Integrierte Anforderungen und Einschränkungen können jedoch nicht mit zusätzlichem Code überwunden werden. Im Folgenden wird argumentiert, dass die Größe von std::bitseteine Konstante für die Kompilierungszeit ist. Daher wird sie zwar nicht als Nachteil gewertet, sie wirkt sich jedoch immer noch auf die Auswahl des Benutzers aus.


Unter ästhetischen Gesichtspunkten (Lesbarkeit, Wartungsfreundlichkeit usw.) gibt es einen Unterschied.

Es ist jedoch nicht ersichtlich, dass der std::bitsetCode den einfachen C-Code sofort gewinnt. Man muss sich größere Teile des Codes (und nicht irgendein Spielzeugbeispiel) ansehen, um festzustellen, ob die Verwendung von std::bitsetdie menschliche Qualität des Quellcodes verbessert hat.


Die Geschwindigkeit der Bitmanipulation hängt vom Codierungsstil ab. Der Codierungsstil wirkt sich sowohl auf die C / C ++ - Bitmanipulation std::bitsetals auch auf diese aus, wie im Folgenden erläutert wird.


Wenn Sie Code operator []schreiben, mit dem Sie jeweils ein Bit lesen und schreiben, müssen Sie dies mehrmals tun, wenn mehr als ein Bit bearbeitet werden muss. Gleiches gilt für den C-Code.

Allerdings bitsethat auch andere Betreiber, wie operator &=, operator <<=etc., die auf der vollen Breite des bitset arbeitet. Da der zugrunde liegende Computer häufig mit 32-Bit-, 64-Bit- und manchmal mit 128-Bit-Code (mit SIMD) gleichzeitig (in der gleichen Anzahl von CPU-Zyklen) arbeiten kann, wurde Code entwickelt, um diese Multi-Bit-Vorgänge zu nutzen kann schneller sein als "schleifenförmiger" Bitmanipulationscode.

Die allgemeine Idee heißt SWAR (SIMD innerhalb eines Registers) und ist ein Unterthema unter Bitmanipulationen .


Einige C ++ - Anbieter implementieren möglicherweise bitsetzwischen 64-Bit und 128-Bit mit SIMD. Einige Anbieter tun dies möglicherweise nicht (aber möglicherweise auch). Wenn Sie wissen müssen, was die Bibliothek des C ++ - Herstellers tut, können Sie sich nur die Demontage ansehen.


Ob std::bitsetes Einschränkungen gibt, kann ich an zwei Beispielen erläutern.

  1. Die Größe von std::bitsetmuss zum Zeitpunkt der Kompilierung bekannt sein. Um ein Array von Bits mit dynamisch gewählter Größe zu erstellen, muss man verwenden std::vector<bool>.
  2. Die aktuelle C ++ - Spezifikation für std::bitsetbietet keine Möglichkeit zum Extrahieren eines aufeinanderfolgenden Schnitts von N Bits aus einem größeren bitsetvon M Bits.

Der erste Punkt ist grundlegend, was bedeutet, dass Menschen, die dynamisch große Bitsets benötigen, andere Optionen wählen müssen.

Die zweite kann überwunden werden, weil man eine Art Adapter schreiben kann, um die Aufgabe auszuführen, selbst wenn der Standard bitsetnicht erweiterbar ist.


Es gibt bestimmte Arten von erweiterten SWAR-Operationen, die nicht ab Werk bereitgestellt werden std::bitset. Über diese Operationen kann man auf dieser Website über Bit-Permutationen lesen . Diese kann man wie gewohnt selbstständig implementieren und aufbauen std::bitset.


In Bezug auf die Diskussion über die Leistung.

Eine Warnung: Viele Leute fragen, warum (etwas) aus der Standardbibliothek viel langsamer ist als irgendein einfacher C-Code. Ich würde das vorausgesetzte Wissen über Mikrobenchmarking hier nicht wiederholen, aber ich habe nur den folgenden Rat: Stellen Sie sicher, dass Sie ein Benchmarking im "Release-Modus" durchführen (mit aktivierten Optimierungen) und dass der Code nicht beseitigt wird (Beseitigung toter Codes) oder nicht beseitigt wird aus einer Schleife gehisst (schleifeninvariante Codebewegung) .

Da wir im Allgemeinen nicht sagen können, ob jemand (im Internet) die Mikrobenchmarks richtig gemacht hat, können wir eine verlässliche Schlussfolgerung nur ziehen, indem wir unsere eigenen Mikrobenchmarks machen, die Details dokumentieren und der öffentlichen Überprüfung und Kritik unterziehen. Es tut nicht weh, Mikrobenchmarks zu wiederholen, die andere schon einmal gemacht haben.

rwong
quelle
Problem Nr. 2 bedeutet auch, dass das Bitset nicht in einem parallelen Setup verwendet werden kann, in dem jeder Thread auf eine Teilmenge des Bitsets angewendet werden soll.
User239558
@ user239558 Ich bezweifle, dass jemand auf dem gleichen parallelisieren möchte std::bitset. Es gibt keine Garantie für die Speicherkonsistenz (in std::bitset), was bedeutet, dass es nicht zwischen Kernen geteilt werden soll. Leute, die es über Kerne hinweg teilen müssen, werden dazu neigen, ihre eigene Implementierung aufzubauen. Wenn Daten zwischen verschiedenen Kernen geteilt werden, ist es üblich, sie an der Cache-Zeilengrenze auszurichten. Andernfalls sinkt die Leistung und es treten weitere Fallstricke auf, die nicht der Atomizität entsprechen. Ich habe nicht genug Wissen, um einen Überblick darüber zu geben, wie eine parallelisierbare Implementierung von erstellt werden kann std::bitset.
Rwong
Für die datenparallele Programmierung ist normalerweise keine Speicherkonsistenz erforderlich. Sie synchronisieren nur zwischen Phasen. Ich möchte unbedingt ein Bitset parallel verarbeiten, ich denke jeder mit einem großen bitsetWillen.
user239558
@ user239558 Das klingt nach Kopieren (der relevante Bitmengenbereich, der von jedem Core verarbeitet werden soll, muss vor Beginn der Verarbeitung kopiert werden). Ich stimme dem zu, obwohl ich denke, dass jeder, der über Parallelisierung nachdenkt, bereits über die Einführung einer eigenen Implementierung nachdenken würde. Im Allgemeinen werden viele C ++ - Standardbibliotheksfunktionen als Basisimplementierungen bereitgestellt. Jeder, der ernstere Bedürfnisse hat, wird seine eigenen umsetzen.
Rwong
nein es wird nicht kopiert. Es greift einfach auf verschiedene Teile einer statischen Datenstruktur zu. Dann ist keine Synchronisation erforderlich.
User239558
2

Dies trifft sicherlich nicht in allen Fällen zu, aber gelegentlich kann ein Algorithmus von der Effizienz des C-Bit-Twiddlings abhängen, um signifikante Leistungssteigerungen zu erzielen. Das erste Beispiel, das mir in den Sinn kommt, ist die Verwendung von Bitboards , clevere Ganzzahlcodierungen von Brettspielpositionen, um Schachmaschinen und dergleichen zu beschleunigen. Hier ist die feste Größe von Integer-Typen kein Problem, da Schachbretter sowieso immer 8 * 8 sind.

Betrachten Sie als einfaches Beispiel die folgende Funktion (entnommen aus dieser Antwort von Ben Jackson ), die eine Connect Four-Position auf Sieg testet:

// return whether newboard includes a win
bool haswon2(uint64_t newboard)
{
    uint64_t y = newboard & (newboard >> 6);
    uint64_t z = newboard & (newboard >> 7);
    uint64_t w = newboard & (newboard >> 8);
    uint64_t x = newboard & (newboard >> 1);
    return (y & (y >> 2 * 6)) | // check \ diagonal
           (z & (z >> 2 * 7)) | // check horizontal -
           (w & (w >> 2 * 8)) | // check / diagonal
           (x & (x >> 2));      // check vertical |
}
David Zhang
quelle
2
Glaubst du, eine std::bitsetwäre langsamer?
Quant
Nun, ausgehend von einem kurzen Blick auf die Quelle, basiert das libc ++ - Bitset auf einem einzelnen size_t oder einem Array von ihnen und würde daher wahrscheinlich zu etwas im Wesentlichen Äquivalentem / Identischem kompiliert, insbesondere auf einem System mit sizeof (size_t) == 8 - also nein, es wäre wahrscheinlich nicht langsamer.
Ryan Pavlik