Szukam skutecznego sposobu na określenie pozycji najmniej znaczącego bitu, który jest ustawiony jako liczba całkowita, np. Dla 0x0FF0 byłoby to 4.
Prosta implementacja jest taka:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Jakieś pomysły, jak wycisnąć z tego kilka cykli?
(Uwaga: to pytanie jest dla ludzi, którzy lubią takie rzeczy, a nie dla ludzi, którzy mówią mi, że xyzoptimization jest zła).
[edytuj] Dziękuję wszystkim za pomysły! Nauczyłem się też kilku innych rzeczy. Chłodny!
c++
c
optimization
bit-manipulation
peterchen
źródło
źródło
Odpowiedzi:
Bit Twiddling Hacks oferuje doskonałą kolekcję, eee, nieco krętych hacków, z dołączoną dyskusją na temat wydajności / optymalizacji. Moim ulubionym rozwiązaniem twojego problemu (z tej strony) jest «pomnóż i wyszukaj»:
Pomocne referencje:
źródło
__builtin_ffsl
lubffsl
?Dlaczego nie skorzystać z wbudowanego ffs ? (Wziąłem stronę podręcznika systemowego z Linuksa, ale jest ona szerzej dostępna).
źródło
Istnieje instrukcja asemblera x86 (
bsf
), która to zrobi. :)Bardziej zoptymalizowany ?!
Dygresja:
Optymalizacja na tym poziomie jest z natury zależna od architektury. Dzisiejsze procesory są zbyt złożone (pod względem przewidywania gałęzi, błędów pamięci podręcznej, przetwarzania potokowego), więc tak trudno jest przewidzieć, który kod jest wykonywany szybciej na jakiej architekturze. Zmniejszenie liczby operacji z 32 do 9 lub podobnych rzeczy może nawet zmniejszyć wydajność na niektórych architekturach. Zoptymalizowany kod w jednej architekturze może spowodować gorszy kod w drugiej. Myślę, że albo zoptymalizowałbyś to dla konkretnego procesora, albo zostawiłbyś to tak, jak jest i pozwolił kompilatorowi wybrać to, co uważa za lepsze.
źródło
Większość współczesnych architektur będzie zawierała instrukcje dotyczące znalezienia pozycji najniższego ustawionego bitu lub najwyższego ustawionego bitu lub zliczania wiodących zer itp.
Jeśli masz jedną instrukcję z tej klasy, możesz tanio naśladować inne.
Poświęć chwilę, aby popracować nad tym na papierze i zdaj sobie sprawę, że
x & (x-1)
wyczyści najniższy ustawiony bit w x i( x & ~(x-1) )
zwróci tylko najniższy ustawiony bit, niezależnie od architektury, długości słowa itp. Wiedząc o tym, używanie sprzętowego licznika początkowego jest trywialne -zeroes / najwyższy-ustawiony-bit, aby znaleźć najniższy ustawiony bit, jeśli nie ma wyraźnej instrukcji, aby to zrobić.Jeśli w ogóle nie ma odpowiedniego wsparcia sprzętowego, implementacja mnożenia i wyszukiwania zer wiodących podana tutaj lub jedna z tych na stronie Bit Twiddling Hacks można w trywialny sposób przekonwertować, aby uzyskać najniższy ustawiony bit przy użyciu powyższych tożsamości i ma tę zaletę, że jest bez gałęzi.
źródło
Mnóstwo rozwiązań, a nie punkt odniesienia w zasięgu wzroku. Powinniście się wstydzić ;-)
Mój komputer to Intel i530 (2,9 GHz) z systemem Windows 7 w wersji 64-bitowej. Skompilowałem z 32-bitową wersją MinGW.
Mój kod:
źródło
BSF
Ma fałszywą zależność od swojego wyjścia (ponieważ rzeczywiste zachowanie gdy input = 0 ma pozostawić wyjście niezmienione). gcc niestety zamienia to w zależność przenoszoną w pętli, nie czyszcząc rejestru między iteracjami pętli. Zatem pętla powinna działać z częstotliwością jeden na 5 cykli, wąskie gardło BSF (3) + CMOV (2) opóźnienieffs()
powinien mieć przepustowość jednego na zegar (3 uops, 1 dla BSF i 2 dla CMOV i mogą działać na różnych portach). Przy takim samym obciążeniu pętli można uruchomić 7 jednostek ALU Uops (na procesorze) z prędkością 3 na zegar. Nad głową dominuje! Źródło: agner.org/optimizebsf ecx, [ebx+edx*4]
nie zostanie potraktowaneecx
jako dane wejściowe, na które musiało czekać. (ECX został ostatnio napisany przez CMOV poprzedniej iteratonu). Ale procesor zachowuje się w ten sposób, aby zaimplementować zachowanie "pozostaw miejsce docelowe niezmodyfikowane, jeśli źródło jest zerowe" (więc nie jest to naprawdę fałszywa dep, jak w przypadku TZCNT; zależność danych jest wymagana, ponieważ nie ma rozgałęziania + spekulacyjne wykonanie przy założeniu że wejście jest niezerowe). Moglibyśmy temu zaradzić, dodającxor ecx,ecx
przed thebsf
, aby zerwać zależność od ECX.Najszybszym rozwiązaniem (nie wewnętrznym / asemblerowym) jest znalezienie najniższego bajtu, a następnie użycie tego bajtu w 256-wpisowej tablicy wyszukiwania. Daje to najgorszy wynik z czterech instrukcji warunkowych, a w najlepszym przypadku 1. Jest to nie tylko najmniejsza liczba instrukcji, ale także najmniejsza liczba rozgałęzień, co jest bardzo ważne na nowoczesnym sprzęcie.
Twoja tabela (256 8-bitowych wpisów) powinna zawierać indeks LSB dla każdej liczby z zakresu 0-255. Sprawdzasz każdy bajt swojej wartości i znajdujesz najniższy niezerowy bajt, a następnie używasz tej wartości do wyszukiwania rzeczywistego indeksu.
Wymaga to 256 bajtów pamięci, ale jeśli szybkość tej funkcji jest tak ważna, to 256 bajtów jest tego warte,
Na przykład
źródło
OMG ma to po prostu spiralne.
W większości tych przykładów brakuje odrobiny zrozumienia działania całego sprzętu.
Za każdym razem, gdy masz gałąź, procesor musi odgadnąć, która gałąź zostanie wybrana. Potok instrukcji jest ładowany instrukcjami prowadzącymi w dół odgadniętej ścieżki. Jeśli CPU źle odgadł, potok instrukcji zostanie opróżniony, a druga gałąź musi zostać załadowana.
Rozważ prostą pętlę while na górze. Domyślam się, że pozostanie w pętli. Przynajmniej raz będzie źle, gdy opuści pętlę. Spowoduje to przepłukanie rury instrukcji. To zachowanie jest nieco lepsze niż zgadywanie, że opuści pętlę, w którym to przypadku będzie przepłukiwał potok z instrukcją przy każdej iteracji.
Ilość utraconych cykli procesora różni się znacznie w zależności od typu procesora. Ale możesz spodziewać się od 20 do 150 utraconych cykli procesora.
Następna gorsza grupa to ta, w której myślisz, że zamierzasz zaoszczędzić kilka iteracji, dzieląc wartość na mniejsze części i dodając kilka kolejnych gałęzi. Każda z tych gałęzi daje dodatkową możliwość przepłukania potoku instrukcji i kosztuje kolejne 20 do 150 cykli zegara.
Zastanówmy się, co się stanie, gdy wyszukasz wartość w tabeli. Prawdopodobnie wartość nie znajduje się obecnie w pamięci podręcznej, a przynajmniej nie przy pierwszym wywołaniu funkcji. Oznacza to, że procesor zatrzymuje się, gdy wartość jest ładowana z pamięci podręcznej. Znowu różni się to w zależności od maszyny. Nowe chipy Intela wykorzystują to w rzeczywistości jako okazję do zamiany wątków, podczas gdy bieżący wątek oczekuje na zakończenie ładowania pamięci podręcznej. Może to być z łatwością droższe niż przepłukiwanie rur z instrukcjami, jednak jeśli wykonujesz tę operację kilka razy, prawdopodobnie nastąpi to tylko raz.
Najwyraźniej najszybszym rozwiązaniem ze stałym czasem jest to, które obejmuje matematykę deterministyczną. Czyste i eleganckie rozwiązanie.
Przepraszam, jeśli to już zostało uwzględnione.
Każdy kompilator, którego używam, z wyjątkiem XCODE AFAIK, ma wbudowane funkcje kompilatora zarówno dla skanowania bitowego w przód, jak i skanowania wstecznego. Będą one kompilować się do pojedynczej instrukcji asemblera na większości sprzętu bez pomijania pamięci podręcznej, bez przewidywania błędów gałęzi i żadnych innych przeszkód generowanych przez programistę.
W przypadku kompilatorów firmy Microsoft użyj _BitScanForward i _BitScanReverse.
W przypadku GCC użyj __builtin_ffs, __builtin_clz, __builtin_ctz.
Ponadto prosimy o powstrzymanie się od publikowania odpowiedzi i potencjalnie wprowadzających w błąd nowoprzybyłych, jeśli nie masz wystarczającej wiedzy na temat omawianego tematu.
Przepraszam, całkowicie zapomniałem podać rozwiązanie. Oto kod, którego używam na iPadzie, który nie ma instrukcji na poziomie asemblera dla tego zadania:
Należy tutaj zrozumieć, że to nie porównanie jest drogie, ale gałąź, która pojawia się po porównaniu. Porównanie w tym przypadku jest zmuszane do wartości 0 lub 1 za pomocą .. == 0, a wynik jest używany do łączenia matematyki, która wystąpiłaby po obu stronach gałęzi.
Edytować:
Powyższy kod jest całkowicie uszkodzony. Ten kod działa i nadal jest wolny od gałęzi (jeśli został zoptymalizowany):
Zwraca wartość -1, jeśli otrzymujesz 0. Jeśli nie zależy ci na 0 lub jesteś szczęśliwy, jeśli masz 31 za 0, usuń obliczenie i0, oszczędzając trochę czasu.
źródło
-O3
godbolt.org/z/gcsUHdZainspirowany tym podobnym postem, który dotyczy wyszukiwania zestawu bitów, oferuję co następuje:
Plusy:
Cons:
Aktualizacja: Jak wskazano w komentarzach, związek jest czystszą implementacją (przynajmniej dla C) i wyglądałby następująco:
Zakłada się 32-bitowe inte z pamięcią little-endian na wszystko (pomyśl o procesorach x86).
źródło
int
jestint32_t
, i że podpisał prawo przesunięcia jest przesunięcie arytmetyczne (w C ++ To realizacji zdefiniowane)Można to zrobić w najgorszym przypadku z mniej niż 32 operacjami:
Zasada: sprawdzenie 2 lub więcej bitów jest tak samo wydajne, jak sprawdzenie 1 bitu.
Na przykład nic nie powstrzymuje Cię przed sprawdzeniem, które grupowanie jest w pierwszej kolejności, a następnie sprawdzeniem każdego bitu od najmniejszego do największego w tej grupie.
Więc ...
jeśli sprawdzasz 2 bity na raz, masz w najgorszym przypadku (Nbits / 2) + 1 sprawdzenie łącznie.
jeśli sprawdzasz 3 bity naraz, masz w najgorszym przypadku (Nbity / 3) + 2 kontrole łącznie.
...
Optymalne byłoby sprawdzenie w grupach po 4 osoby, co wymagałoby w najgorszym przypadku 11 operacji zamiast 32.
Najlepszym przypadkiem jest przejście od 1 testu algorytmów do 2 sprawdzeń, jeśli używasz tego pomysłu na grupowanie. Ale ten dodatkowy 1 czek w najlepszym przypadku jest tego wart, aby uzyskać oszczędności w najgorszym przypadku.
Uwaga: piszę to w całości zamiast używać pętli, ponieważ jest to bardziej wydajne w ten sposób.
źródło
Dlaczego nie skorzystać z wyszukiwania binarnego ? To zawsze zakończy się po 5 operacjach (zakładając rozmiar int 4 bajty):
źródło
Inna metoda (dzielenie modułu i wyszukiwanie) zasługuje na specjalną wzmiankę z tego samego linku, który udostępnił @ anton-tykhyy. ta metoda jest bardzo podobna pod względem wydajności do metody mnożenia i wyszukiwania DeBruijn z niewielką, ale istotną różnicą.
dzielenie modułu i wyszukiwanie
metoda dzielenia modułu i wyszukiwania zwraca różne wartości dla v = 0x00000000 i v = FFFFFFFF, podczas gdy metoda mnożenia i wyszukiwania DeBruijn zwraca zero na obu wejściach.
test:-
źródło
mod
jest wolny. Zamiast tego można użyć oryginalnej metody mnożenia i wyszukiwania i odejmowania!v
od,r
aby obsłużyć przypadki skrajne.Według strony Chess Programming BitScan i moich własnych pomiarów, odejmowanie i xor jest szybsze niż negowanie i maskowanie.
(Zauważ, że jeśli zamierzasz liczyć końcowe zera w
0
, metoda, którą mam, zwraca,63
podczas gdy negacja i maska powracają0
.)Oto 64-bitowe odejmowanie i xor:
Dla porównania, oto 64-bitowa wersja metody negacji i maski:
źródło
(v ^ (v-1))
działa pod warunkiemv != 0
. W takim przypadkuv == 0
zwraca 0xFF .... FF, a jednocześnie(v & -v)
daje zero (co zresztą też jest błędne, buf przynajmniej prowadzi do rozsądnego wyniku).v ^ (v-1)
, więc nie ma możliwości ich rozróżnienia. W moim scenariuszu zero nigdy nie zostanie wprowadzone.Możesz sprawdzić, czy któryś z bitów niższego rzędu jest ustawiony. Jeśli tak, spójrz na niższą kolejność pozostałych bitów. na przykład,:
32bit int - sprawdź, czy któreś z pierwszych 16 jest ustawione. Jeśli tak, sprawdź, czy ustawiono którykolwiek z pierwszych 8. jeśli tak, ....
jeśli nie, sprawdź, czy któreś z 16 górnych są ustawione.
Zasadniczo jest to wyszukiwanie binarne.
źródło
Zobacz moją odpowiedź tutaj, aby dowiedzieć się, jak to zrobić za pomocą pojedynczej instrukcji x86, z wyjątkiem tego, że aby znaleźć najmniej znaczący zestaw bitów, będziesz potrzebować instrukcji
BSF
(„skanowanie bitów do przodu”) zamiastBSR
opisanej w tym miejscu.źródło
Jeszcze inne rozwiązanie, nie najszybsze możliwe, ale wydaje się całkiem dobre.
Przynajmniej nie ma gałęzi. ;)
źródło
1
s od najmniej znaczącej 1 do LSB, użyj((x & -x) - 1) << 1
zamiast tegox ^ (x-1)
50% wszystkich liczb wróci w pierwszym wierszu kodu.
75% wszystkich liczb powróci w pierwszych 2 wierszach kodu.
87% wszystkich liczb wróci w pierwszych 3 wierszach kodu.
94% wszystkich liczb wróci w pierwszych 4 wierszach kodu.
97% wszystkich liczb wróci w pierwszych 5 wierszach kodu.
itp.
Myślę, że ludzie, którzy narzekają na to, jak nieefektywny jest najgorszy scenariusz dla tego kodu, nie rozumieją, jak rzadki będzie ten stan.
źródło
Znalazłem tę sprytną sztuczkę przy użyciu „magicznych masek” w „Sztuce programowania, część 4”, która robi to w czasie O (log (n)) dla liczby n-bitowej. [z log (n) dodatkową spacją]. Typowe rozwiązania sprawdzające ustawiony bit to O (n) lub wymagające O (n) dodatkowej przestrzeni na tablicę przeglądową, więc jest to dobry kompromis.
Magiczne maski:
Kluczowa idea: liczba końcowych zer w x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
źródło
Jeśli C ++ 11 jest dla Ciebie dostępny, kompilator czasami może wykonać to zadanie za Ciebie :)
Wynik jest indeksem od 1.
źródło
ffs()
w czasie kompilacji, więc nie musisz go używać do pracy ciągłej propagacji. (Trzeba unikać inline asm, oczywiście). Jeśli naprawdę potrzebują czegoś, co działa jak C ++ 11constexpr
, nadal można używać GNU C__builtin_ffs
.Dotyczy to odpowiedzi @Anton Tykhyy
Oto moja implementacja constexpr w C ++ 11 eliminująca rzutowanie i usuwająca ostrzeżenie w VC ++ 17 przez obcięcie wyniku 64-bitowego do 32 bitów:
Aby obejść problem 0x1 i 0x0 zwracających 0, możesz zrobić:
ale jeśli kompilator nie może lub nie może wstępnie przetworzyć wywołania, doda kilka cykli do obliczenia.
Na koniec, jeśli jesteś zainteresowany, oto lista statycznych potwierdzeń, które sprawdzają, czy kod robi to, co ma:
źródło
Oto jedna prosta alternatywa, mimo że znajdowanie dzienników jest trochę kosztowne.
źródło
Niedawno widzę, że premier Singapuru opublikował program, który napisał na Facebooku, jest jedna linijka, aby o tym wspomnieć ..
Logika to po prostu „wartość i -wartość”, przypuśćmy, że masz 0x0FF0, a następnie 0FF0 i (F00F + 1), co równa się 0x0010, co oznacza, że najniższa 1 znajduje się w czwartym bicie .. :)
źródło
Jeśli masz zasoby, możesz poświęcić pamięć, aby poprawić prędkość:
Uwaga: ta tabela zużyłaby co najmniej 4 GB (16 GB, jeśli pozostawimy zwracany typ jako
unsigned
). To jest przykład wymiany jednego ograniczonego zasobu (RAM) na inny (szybkość wykonywania).Jeśli twoja funkcja musi pozostać przenośna i działać tak szybko, jak to możliwe za wszelką cenę, to byłaby droga do zrobienia. W większości rzeczywistych aplikacji tabela 4 GB jest nierealna.
źródło
:)
@Dan: Masz rację co do buforowania pamięci. Zobacz komentarz Mikeage powyżej.