Mam kod mniej więcej taki:
#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 robi inteligentną rzecz i kompiluje to do pojedynczej and
instrukcji (która jest następnie wstawiana wszędzie indziej):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
Ale każda wersja GCC, którą próbowałem, kompiluje to do ogromnego bałaganu, który obejmuje obsługę błędów, która powinna być statycznie DCE. W innym kodzie nawet umieści important_bits
odpowiednik jako dane zgodnie z kodem!
.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*, ...)
Jak napisać ten kod, aby oba kompilatory działały właściwie? W przeciwnym razie jak mam to napisać, aby było jasne, szybkie i możliwe do utrzymania?
c++
c++11
bit-manipulation
Alex Reinking
źródło
źródło
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| w każdym wierszu i wyrównaj stałe nazwy w różnych wierszach, co będzie łatwiejsze dla oczu.int
wynikiem operacji na bitach MOŻE BYĆint
LUB możelong long
zależeć od wartości i formalnieenum
nie jest odpowiednikiemint
stałej. clang woła o „jak gdyby”, gcc pozostaje pedantycznyOdpowiedzi:
Najlepsza wersja to c ++ 17:
Następnie
z powrotem w c ++ 14, możemy zrobić tę dziwną sztuczkę:
lub, jeśli utknęliśmy c ++ 11możemy to rozwiązać rekurencyjnie:
Godbolt ze wszystkimi 3 - możesz przełączyć CPP_VERSION zdefiniować i uzyskać identyczny montaż.
W praktyce użyłbym najnowocześniejszego, jak tylko potrafię. 14 pokonuje 11, ponieważ nie mamy rekursji, a zatem O (n ^ 2) długości symbolu (co może spowodować eksplozję czasu kompilacji i wykorzystania pamięci kompilatora); 17 bije 14, ponieważ kompilator nie musi usuwać tej tablicy martwego kodu, a ta sztuczka tablicowa jest po prostu brzydka.
Spośród nich 14 jest najbardziej zagmatwanych. Tutaj tworzymy anonimową tablicę wszystkich zer, tymczasem jako efekt uboczny konstruujemy nasz wynik, a następnie odrzucamy tablicę. Wyrzucona tablica zawiera liczbę 0 równą rozmiarowi naszej paczki, plus 1 (którą dodajemy, abyśmy mogli obsłużyć puste paczki).
Szczegółowe wyjaśnienie, co jest c ++ 14wersja robi. To jest sztuczka / hack, a fakt, że musisz to zrobić, aby rozszerzyć pakiety parametrów z wydajnością w C ++ 14, jest jednym z powodów, dla których wyrażenia fold zostały dodane wc ++ 17.
Najlepiej to zrozumieć od podszewki:
to właśnie aktualizuje
r
się1<<indexes
za ustaloną indeksu.indexes
jest pakietem parametrów, więc będziemy musieli go rozszerzyć.Reszta pracy polega na dostarczeniu pakietu parametrów do rozszerzenia
indexes
wewnątrz.Jeden krok do przodu:
tutaj rzutujemy nasze wyrażenie na
void
, co oznacza, że nie obchodzi nas jego wartość zwracana (chcemy tylko efektu ubocznego ustawieniar
- w C ++ wyrażenia takie jaka |= b
również zwracają wartość, którą ustawilia
).Następnie używamy operatora przecinka
,
i0
odrzucamyvoid
„wartość”, a zwracamy wartość0
. Więc to jest wyrażenie, którego wartość jest0
i jako efekt uboczny obliczania0
, ustawia ją niecor
.W tym momencie rozszerzamy pakiet parametrów
indexes
. Więc otrzymujemy:w
{}
. To użycie nie,
jest operatorem przecinka, ale raczej separatorem elementu tablicy. To jest s, które również ustawia bity jako efekt uboczny. Następnie przypisujemy instrukcje konstrukcji tablicy do tablicy .sizeof...(indexes)+1
0
r
{}
discard
Następnie rzucamy
discard
navoid
- większość kompilatorów ostrzeże Cię, jeśli utworzysz zmienną i nigdy jej nie przeczytasz. Wszyscy kompilatorzy nie będą narzekać, jeśli rzucisz to navoid
, jest to rodzaj powiedzenia „Tak, wiem, nie używam tego”, więc to pomija ostrzeżenie.źródło
((1ull<<indexes)|...|0ull)
, jest to „fałdowe wyrażenie” . W szczególności jest to „binarne prawe zagięcie” i powinno być analizowane jako(pack
op
...
op
init)
Optymalizacja, której szukasz, wydaje się być peelingiem w pętli, który jest włączony
-O3
lub ręcznie za pomocą-fpeel-loops
. Nie jestem pewien, dlaczego należy to do zakresu peelingu pętli, a nie rozwijania pętli, ale prawdopodobnie nie chce rozwinąć pętli z nielokalnym przepływem kontrolnym w niej (jak to jest, potencjalnie, z kontroli zasięgu).Domyślnie jednak GCC przestaje być w stanie usunąć wszystkie iteracje, co najwyraźniej jest konieczne. Eksperymentalnie przekazanie
-O2 -fpeel-loops --param max-peeled-insns=200
(wartość domyślna to 100) powoduje wykonanie zadania przy użyciu oryginalnego kodu: https://godbolt.org/z/NNWrgaźródło
-O3 -fpeel-loops --param max-peeled-insns=200
zawodzi ... To-ftree-slp-vectorize
najwyraźniej przez.jeśli używanie tylko C ++ 11 jest koniecznością,
(&a)[N]
jest sposobem na przechwytywanie tablic. Pozwala to na napisanie jednej funkcji rekurencyjnej bez korzystania z funkcji pomocniczych:przypisanie go do
constexpr auto
:Test
Wynik
naprawdę trzeba docenić zdolność C ++ do obliczania wszystkiego, co można obliczyć w czasie kompilacji. To na pewno nadal mi się podoba ( <> ).
W przypadku nowszych wersji C ++ 14 i C ++ 17 odpowiedź Yakka już wspaniale to pokrywa.
źródło
apply_known_mask
faktycznie optymalizuje?constexpr
. I chociaż to teoretycznie nie wystarczy, wiemy, że GCC jest w stanie ocenićconstexpr
zgodnie z zamierzeniami.Zachęcam do napisania odpowiedniego
EnumSet
typu.Pisanie podstawowego
EnumSet<E>
języka w C ++ 14 (wzwyż) na podstawiestd::uint64_t
jest trywialne:Pozwala to napisać prosty kod:
W C ++ 11 wymaga pewnych zwojów, ale mimo to pozostaje możliwe:
I jest wywoływany przez:
Nawet GCC w trywialny sposób generuje
and
instrukcję w-O1
godbolt :źródło
constexpr
kodu nie jest legalna. Mam na myśli, że niektóre mają 2 stwierdzenia! (C ++ 11 constexpr do dupy)EnumSet<E>
nie używa wartościE
as bezpośrednio, ale zamiast tego używa1 << e
. To zupełnie inna domena, co w rzeczywistości sprawia, że klasa jest tak cenna => nie ma szans na przypadkowe indeksowanie przeze
zamiast1 << e
.Od C ++ 11 możesz również użyć klasycznej techniki TMP:
Link do Compiler Explorer: https://godbolt.org/z/Gk6KX1
Zaletą tego podejścia w porównaniu z funkcją constexpr szablonu jest to, że kompilacja jest potencjalnie nieco szybsza z powodu reguły Chiel .
źródło
Jest tu sporo do „sprytnych” pomysłów. Prawdopodobnie nie pomagasz w utrzymaniu, przestrzegając ich.
jest
o wiele łatwiejsze do napisania niż
?
Wtedy żadna reszta kodu nie jest potrzebna.
źródło