Ich habe einen Code, der mehr oder weniger so ist:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits[] = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang> = 3.6 macht das and
Schlaue und kompiliert dies zu einer einzigen Anweisung (die dann überall sonst eingefügt wird):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
Aber jede Version von GCC, die ich versucht habe, kompiliert dies zu einem enormen Durcheinander, das Fehlerbehandlung beinhaltet, die statisch DCE-fähig sein sollte. In anderem Code wird sogar das important_bits
Äquivalent als Daten in Übereinstimmung mit dem Code platziert!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
Wie soll ich diesen Code schreiben, damit beide Compiler das Richtige tun können? Wenn dies nicht der Fall ist, wie soll ich das schreiben, damit es klar, schnell und wartbar bleibt?
c++
c++11
bit-manipulation
Alex Reinking
quelle
quelle
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| pro Zeile und richten Sie die konstanten Namen auf den verschiedenen Zeilen aus, was für die Augen einfacher wäre.int
Ergebnis der Bitverschiebung hier ist ein Ergebnis der Bitoperation, dieint
möglicherweiselong long
vom Wert abhängt und formalenum
ist nicht gleichbedeutend mit einerint
Konstanten. Clang fordert "als ob", gcc bleibt pedantischAntworten:
Beste Version ist c ++ 17::
Dann
zurück in c ++ 14können wir diesen seltsamen Trick machen:
oder, wenn wir feststecken c ++ 11können wir es rekursiv lösen:
Godbolt mit allen 3 - Sie können CPP_VERSION define wechseln und identische Assembly erhalten.
In der Praxis würde ich das modernste verwenden, das ich konnte. 14 schlägt 11, weil wir keine Rekursion und damit keine O (n ^ 2) -Symbollänge haben (was die Kompilierungszeit und die Verwendung des Compilerspeichers explodieren lassen kann); 17 schlägt 14, weil der Compiler dieses Array nicht mit totem Code eliminieren muss und dieser Array-Trick einfach hässlich ist.
Von diesen 14 ist die verwirrendste. Hier erstellen wir ein anonymes Array aller Nullen, während wir als Nebeneffekt unser Ergebnis konstruieren und das Array dann verwerfen. Das verworfene Array enthält eine Anzahl von Nullen, die der Größe unseres Pakets entspricht, plus 1 (die wir hinzufügen, damit wir leere Pakete verarbeiten können).
Eine ausführliche Erklärung, was die c ++ 14Version tut. Dies ist ein Trick / Hack, und die Tatsache, dass Sie dies tun müssen, um Parameterpakete in C ++ 14 effizient zu erweitern, ist einer der Gründe, warum Fold-Ausdrücke hinzugefügt wurdenc ++ 17.
Es ist am besten von innen nach außen zu verstehen:
dieses Updates nur
r
mit1<<indexes
für einen festen Index.indexes
ist ein Parameterpaket, daher müssen wir es erweitern.Der Rest der Arbeit besteht darin, ein Parameterpaket bereitzustellen, das
indexes
innerhalb von erweitert werden kann.Ein Schritt heraus:
Hier setzen wir unseren Ausdruck auf
void
, um anzuzeigen, dass uns der Rückgabewert egal ist (wir möchten nur den Nebeneffekt der Einstellungr
- in C ++ geben Ausdrücke wiea |= b
auch den Wert zurück, auf den sie gesetzta
sind).Dann verwenden wir den Komma-Operator
,
und0
, um denvoid
"Wert" zu verwerfen und den Wert zurückzugeben0
. Dies ist also ein Ausdruck, dessen Wert ist,0
und als Nebeneffekt der Berechnung0
setzt er ein wenig einr
.An dieser Stelle erweitern wir das Parameterpaket
indexes
. So bekommen wir:in der
{}
. Diese Verwendung von,
ist nicht der Kommaoperator, sondern das Arrayelementtrennzeichen. Dies istsizeof...(indexes)+1
0
s, dasr
als Nebeneffekt auch Bits setzt . Anschließend weisen wir die{}
Array-Konstruktionsanweisungen einem Array zudiscard
.Als nächstes setzen wir
discard
aufvoid
- die meisten Compiler werden Sie warnen, wenn Sie eine Variable erstellen und sie nie lesen. Alle Compiler werden sich nicht beschweren, wenn Sie es besetzenvoid
. Es ist eine Art zu sagen "Ja, ich weiß, ich benutze das nicht", also unterdrückt es die Warnung.quelle
((1ull<<indexes)|...|0ull)
, ist es ein "Falzausdruck" . Insbesondere ist es eine "binäre rechte Falte" und es sollte analysiert werden als(pack
op
...
op
init)
Die Optimierung, nach der Sie suchen, scheint das Loop-Peeling zu sein, das bei
-O3
oder manuell mit aktiviert wird-fpeel-loops
. Ich bin mir nicht sicher, warum dies eher in den Bereich des Schleifenschälens als des Abrollens von Schleifen fällt, aber möglicherweise ist es nicht bereit, eine Schleife mit nicht lokalem Kontrollfluss darin abzuwickeln (wie dies möglicherweise aus der Bereichsprüfung hervorgeht).Standardmäßig kann GCC jedoch nicht alle Iterationen ablösen, was anscheinend erforderlich ist. Experimentell wird durch Übergeben
-O2 -fpeel-loops --param max-peeled-insns=200
(der Standardwert ist 100) die Aufgabe mit Ihrem ursprünglichen Code erledigt: https://godbolt.org/z/NNWrgaquelle
-O3 -fpeel-loops --param max-peeled-insns=200
scheitert ... Es liegt-ftree-slp-vectorize
anscheinend an.Wenn nur C ++ 11 verwendet wird, ist dies ein Muss
(&a)[N]
, um Arrays zu erfassen. Auf diese Weise können Sie eine einzelne rekursive Funktion schreiben, ohne Hilfsfunktionen zu verwenden:Zuweisen zu einem
constexpr auto
:Prüfung
Ausgabe
Man muss wirklich die Fähigkeit von C ++ schätzen, alles zu berechnen, was zur Kompilierungszeit berechenbar ist. Es ist sicherlich immer noch umwerfend ( <> ).
Für die späteren Versionen von C ++ 14 und C ++ 17 deckt Yakks Antwort dies bereits wunderbar ab.
quelle
apply_known_mask
tatsächlich optimiert wird?constexpr
. Und obwohl dies theoretisch nicht ausreicht, wissen wir, dass GCC durchaus in der Lage ist,constexpr
wie beabsichtigt zu bewerten .Ich würde Sie ermutigen, einen richtigen
EnumSet
Typ zu schreiben .Das Schreiben eines Basic
EnumSet<E>
in C ++ 14 (ab) basierend aufstd::uint64_t
ist trivial:Auf diese Weise können Sie einfachen Code schreiben:
In C ++ 11 erfordert es einige Windungen, bleibt aber dennoch möglich:
Und wird aufgerufen mit:
Sogar GCC generiert trivial eine
and
Anweisung bei-O1
Godbolt :quelle
constexpr
Codes nicht legal. Ich meine, einige haben 2 Aussagen! (C ++ 11 constexpr saugte)EnumSet<E>
ein Wert vonE
nicht direkt als Wert verwendet wird, sondern stattdessen verwendet wird1 << e
. Es ist eine andere Domain zusammen, was eigentlich ist , was die Klasse so wertvoll = macht> keine Chance, aus Versehen der Indizierung durche
statt1 << e
.Seit C ++ 11 können Sie auch die klassische TMP-Technik verwenden:
Link zum Compiler Explorer: https://godbolt.org/z/Gk6KX1
Der Vorteil dieses Ansatzes gegenüber der Funktion constexpr für Vorlagen besteht darin, dass das Kompilieren aufgrund der Chiel-Regel möglicherweise etwas schneller ist .
quelle
Hier gibt es einige weit zu "kluge" Ideen. Sie helfen wahrscheinlich nicht bei der Wartbarkeit, indem Sie ihnen folgen.
ist
so viel einfacher zu schreiben als
?
Dann wird der Rest des Codes nicht benötigt.
quelle