Ostatnio pracowałem nad osobistym projektem, kiedy natknąłem się na dziwny problem.
W bardzo ciasnej pętli mam liczbę całkowitą o wartości od 0 do 15. Muszę uzyskać -1 dla wartości 0, 1, 8 oraz 9 i 1 dla wartości 4, 5, 12 i 13.
Zwróciłem się do Godbolta, aby sprawdzić kilka opcji i byłem zaskoczony, że wydawało się, że kompilator nie może zoptymalizować instrukcji switch w taki sam sposób jak łańcuch if.
Link jest tutaj: https://godbolt.org/z/WYVBFl
Kod to:
const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
int a(int num) {
return lookup[num & 0xF];
}
int b(int num) {
num &= 0xF;
if (num == 0 || num == 1 || num == 8 || num == 9)
return -1;
if (num == 4 || num == 5 || num == 12 || num == 13)
return 1;
return 0;
}
int c(int num) {
num &= 0xF;
switch (num) {
case 0: case 1: case 8: case 9:
return -1;
case 4: case 5: case 12: case 13:
return 1;
default:
return 0;
}
}
Myślałem, że b i c przyniosą takie same wyniki, i miałem nadzieję, że sam potrafię odczytać hacki bitów, aby samemu wymyślić wydajną implementację, ponieważ moje rozwiązanie (instrukcja switch - w innej formie) było dość wolne.
Dziwnie, b
skompilowany do hacków bitowych, podczas gdy c
był albo prawie niezoptymalizowany, albo zredukowany do innego przypadku a
uzależnienia od docelowego sprzętu.
Czy ktoś może wyjaśnić, dlaczego istnieje taka rozbieżność? Jaki jest „prawidłowy” sposób optymalizacji tego zapytania?
EDYTOWAĆ:
Wyjaśnienie
Chcę, aby rozwiązanie przełącznika było najszybsze lub podobnie „czyste”. Jednak po skompilowaniu z optymalizacjami na moim komputerze rozwiązanie jest znacznie szybsze.
Napisałem szybki program do zademonstrowania, a TIO ma takie same wyniki, jak znajduję lokalnie: Wypróbuj online!
Dzięki static inline
tablicy odnośników trochę przyspieszysz: wypróbuj online!
źródło
-O3
i skompilowałem goc
do czegoś prawdopodobnie gorszego niża
lubb
(c
miał dwa skoki warunkowe plus kilka bitowych manipulacji, w porównaniu do tylko jednego skoku warunkowego i prostszego manipulowania bitamib
), ale nadal lepszy niż naiwny przedmiot według testów przedmiotów. Nie jestem pewien, o co tak naprawdę tutaj prosisz; prosty fakt jest taki, że kompilator optymalizacyjny może zmienić dowolne z nich w dowolne inne, jeśli tak zdecyduje, i nie ma twardych i szybkich reguł dotyczących tego, co zrobi lub nie zrobi.if
wciąż bijeswitch
(dziwne wyszukiwanie staje się jeszcze szybsze) [TIO do śledzenia]Odpowiedzi:
Jeśli jawnie wyliczysz wszystkie przypadki, gcc jest bardzo wydajny:
jest po prostu skompilowany w prostej gałęzi indeksowanej:
Zauważ, że jeśli nie
default:
ma komentarza, gcc wraca do swojej zagnieżdżonej wersji gałęzi.źródło
pslld
/psrad
lub ich 8-drożnych odpowiedników AVX2. Wiele zależy od innych cech Twojego kodu.Kompilatory C mają specjalne przypadki
switch
, ponieważ oczekują od programistów zrozumienia idiomuswitch
i go wykorzystają.Kod jak:
nie przejdzie przeglądu przez kompetentnych programistów C; trzech lub czterech recenzentów jednocześnie wykrzyknęłoby „powinno to być
switch
!”Kompilatory C nie warto analizować struktury
if
instrukcji do konwersji do tabeli skoków. Warunki muszą być w sam raz, a ilość możliwych zmian w wieluif
stwierdzeniach jest astronomiczna. Analiza jest zarówno skomplikowana, jak i może okazać się negatywna (jak w: „nie, nie możemy przekonwertować tychif
s naswitch
”).źródło
if
, jeśli w ogóle możliwe.static
i skorzystaj z inicjalizatorów C99, jeśli chcesz, aby było trochę bardziej jasne, co przypisujesz, i jest to całkowicie w porządku.if
(patrz edycja). @R .. Opracowałem pełne rozwiązanie bitowe dla kompilatora, którego teraz używam. Niestety w moim przypadku są toenum
wartości, a nie liczby całkowite, więc hacków bitowych nie da się łatwo utrzymać.Poniższy kod obliczy Twoje wyszukiwanie bez gałęzi, bez LUT, w ~ 3 cyklach zegara, ~ 4 przydatnych instrukcjach i ~ 13 bajtach wysoce-
inline
kodu maszynowego x86.To zależy od reprezentacji liczb całkowitych dopełniacza 2.
Musisz jednak upewnić się, że
u32
i is32
typedefs naprawdę wskazują 32-bitowe typy całkowite bez znaku i ze znakiem.stdint.h
typyuint32_t
iint32_t
byłyby odpowiednie, ale nie mam pojęcia, czy nagłówek jest dostępny.Przekonaj się tutaj: https://godbolt.org/z/AcJWWf
Na wybór stałej
Twoje wyszukiwanie obejmuje 16 bardzo małych stałych od -1 do +1 włącznie. Każdy mieści się w obrębie 2 bitów, a jest ich 16, które możemy przedstawić następująco:
Umieszczając je z indeksem 0 najbliższym najbardziej znaczącym bitowi, pojedyncze przesunięcie
2*num
spowoduje umieszczenie bitu znakowego twojej 2-bitowej liczby w bicie znakowym rejestru. Przesunięcie w prawo 2-bitowej liczby o 32-2 = 30 bitów - znak rozszerza ją do pełnegoint
, kończąc lewę.źródło
magic
komentarzem wyjaśniającym, jak go zregenerować. Czy możesz wyjaśnić, jak to wymyśliłeś?!!(12336 & (1<<x))-!!(771 & (1<<x));
Możesz stworzyć ten sam efekt używając tylko arytmetyki:
Mimo to, technicznie rzecz biorąc, jest to nadal (bitowe) wyszukiwanie.
Jeśli powyższe wydaje się zbyt tajemnicze, możesz także:
źródło