Podsumowanie:
Szukam najszybszego sposobu obliczenia
(int) x / (int) y
bez wyjątku dla y==0
. Zamiast tego chcę po prostu arbitralnego wyniku.
Tło:
Podczas kodowania algorytmów przetwarzania obrazu często muszę podzielić przez (skumulowaną) wartość alfa. Najprostszym wariantem jest zwykły kod C z arytmetyką liczb całkowitych. Mój problem polega na tym, że zwykle otrzymuję błąd dzielenia przez zero dla pikseli wynikowych z alpha==0
. Jednak są to dokładnie piksele, w których wynik nie ma żadnego znaczenia: nie obchodzą mnie wartości kolorów pikseli z alpha==0
.
Detale:
Szukam czegoś takiego:
result = (y==0)? 0 : x/y;
lub
result = x / MAX( y, 1 );
x i y są dodatnimi liczbami całkowitymi. Kod jest wykonywany ogromną liczbę razy w zagnieżdżonej pętli, więc szukam sposobu na pozbycie się rozgałęzień warunkowych.
Kiedy y nie przekracza zakresu bajtów, jestem zadowolony z rozwiązania
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
Ale to oczywiście nie działa dobrze dla większych zakresów.
Wydaje mi się, że ostatnie pytanie brzmi: jaki jest najszybszy sposób na zmianę wartości 0 na dowolną inną liczbę całkowitą, pozostawiając wszystkie inne wartości bez zmian?
Wyjaśnienia
Nie jestem w 100% pewien, czy rozgałęzianie jest zbyt drogie. Jednak używane są różne kompilatory, więc wolę testy porównawcze z niewielkimi optymalizacjami (co jest rzeczywiście wątpliwe).
Z pewnością kompilatory są świetne, jeśli chodzi o manipulowanie bitami, ale nie mogę wyrazić wyniku "nie przejmuję się" w C, więc kompilator nigdy nie będzie w stanie wykorzystać pełnego zakresu optymalizacji.
Kod powinien być w pełni kompatybilny z C, główne platformy to Linux 64-bitowy z gcc i clang oraz MacOS.
źródło
y += !y
? Żadna gałąź nie była potrzebna do obliczenia tego. Możesz porównaćx / (y + !y)
z,x / max(y, 1)
a może takżey ? (x/y) : 0
. Myślę, że w żadnym z nich nie będzie odgałęzienia, przynajmniej przy włączonych optymalizacjach.0
sekcje alfa są duże i ciągłe. Jest miejsce na majstrowanie przy mikrooptymalizacjach, a operacje na piksel są właśnie tym miejscem.Odpowiedzi:
Zainspirowany niektórymi komentarzami pozbyłem się gałęzi na moim Pentium i
gcc
kompilatorze za pomocąKompilator zasadniczo rozpoznaje, że może użyć flagi warunku testu w dodatku.
Na życzenie montaż:
Ponieważ okazało się, że jest to popularne pytanie i odpowiedź, opowiem o tym nieco więcej. Powyższy przykład jest oparty na idiomie programowania, który rozpoznaje kompilator. W powyższym przypadku w arytmetyce całkowej używane jest wyrażenie boolowskie, a do tego celu w sprzęcie wymyślono użycie flag warunków. Ogólnie flagi warunków są dostępne tylko w C za pomocą idiomu. Dlatego tak trudno jest stworzyć przenośną bibliotekę liczb całkowitych o wielokrotnej precyzji w C bez uciekania się do asemblacji (inline). Domyślam się, że większość przyzwoitych kompilatorów zrozumie powyższy idiom.
Innym sposobem na unikanie rozgałęzień, jak również zauważono w niektórych z powyższych komentarzy, jest wykonanie predykatu. Dlatego wziąłem pierwszy kod Philippa i mój kod i przepuściłem go przez kompilator z ARM i kompilator GCC dla architektury ARM, która zawiera predykowane wykonanie. Oba kompilatory unikają gałęzi w obu przykładach kodu:
Wersja Filipa z kompilatorem ARM:
Wersja Filipa z GCC:
Mój kod z kompilatorem ARM:
Mój kod z GCC:
Wszystkie wersje nadal wymagają rozgałęzienia do procedury dywizji, ponieważ ta wersja ARM nie ma sprzętu do podziału, ale test
y == 0
jest w pełni zaimplementowany poprzez wykonanie predykatu.źródło
constexpr
i uniknąć niepotrzebnych rzutów typu:template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }
A jeśli chcesz255
,(lhs)/(rhs+!rhs) & -!rhs
|
nie&
. Ups -( (lhs)/(rhs+!rhs) ) | -!rhs
należy ustawić wartość na0xFFFFFFF
jeślirhs
jest0
ilhs/rhs
jeślirhs!=0
.Oto kilka konkretnych liczb w systemie Windows używającym GCC 4.7.2:
Zauważ, że celowo nie dzwonię
srand()
, więcrand()
zawsze zwraca dokładnie te same wyniki. Zauważ również, że-DCHECK=0
liczy tylko zera, więc jest oczywiste, jak często się pojawiał.Teraz kompilujemy i mierzymy czas na różne sposoby:
pokazuje dane wyjściowe, które można podsumować w tabeli:
Jeśli zera są rzadkie,
-DCHECK=2
wersja działa źle. Gdy zera zaczną pojawiać się więcej,-DCHECK=2
sprawa zaczyna działać znacznie lepiej. Wśród innych opcji naprawdę nie ma dużej różnicy.Bo
-O3
to jednak inna historia:W tym przypadku sprawdzenie 2 nie ma wady w porównaniu z innymi kontrolami i zachowuje korzyści, ponieważ zera stają się bardziej powszechne.
Jednak naprawdę powinieneś dokonać pomiaru, aby zobaczyć, co dzieje się z twoim kompilatorem i reprezentatywnymi przykładowymi danymi.
źródło
d=0
losowych, zamiast prawie zawszed!=0
, a zobaczysz więcej błędów przewidywania gałęzi. Przewidywanie gałęzi jest świetne, jeśli prawie zawsze przestrzega się jednej gałęzi lub jeśli śledzenie jednej lub drugiej gałęzi jest naprawdęd
Iteracja to pętla wewnętrzna, więc obserwacjed == 0
są rozmieszczone równomiernie. I czy 50% przypadków jestd == 0
realistyczne?0.002%
spraw jestd==0
realistyczne? Są rozprowadzane po każdym 65000 iteracji, które trafią w Twojąd==0
sprawę. Chociaż50%
może nie zdarzać się często10%
lub1%
może się zdarzyć łatwo, a nawet90%
lub99%
. Test, tak jak jest wyświetlany, tak naprawdę sprawdza tylko „jeśli w zasadzie nigdy, przenigdy nie zejdziesz w dół gałęzi, czy przewidywanie gałęzi sprawia, że usuwanie gałęzi jest bezcelowe?”, Na co odpowiedź brzmi „tak, ale to nie jest interesujące”.Bez znajomości platformy nie ma sposobu, aby poznać dokładną najbardziej wydajną metodę, jednak w systemie ogólnym może to być zbliżone do optymalnego (przy użyciu składni Intel assembler):
(załóżmy, że jest dzielnik,
ecx
a dywidenda jesteax
)Cztery nierozgałęzione instrukcje z jednym cyklem plus dzielenie. Iloraz będzie w,
eax
a reszta będzieedx
na końcu. (Ten rodzaj pokazuje, dlaczego nie chcesz wysyłać kompilatora do pracy mężczyzny).źródło
Zgodnie z tym linkiem możesz po prostu zablokować sygnał SIGFPE za pomocą
sigaction()
(sam tego nie próbowałem, ale uważam, że powinno działać).Jest to najszybsze możliwe podejście, jeśli błędy dzielenia przez zero są niezwykle rzadkie: płacisz tylko za podziały przez zero, a nie za prawidłowe podziały, normalna ścieżka wykonania w ogóle się nie zmienia.
Jednak system operacyjny będzie zaangażowany w każdy ignorowany wyjątek, co jest kosztowne. Myślę, że powinieneś mieć co najmniej tysiąc dobrych podziałów na dział przez zero, które ignorujesz. Jeśli wyjątki są częstsze, prawdopodobnie zapłacisz więcej, ignorując wyjątki niż sprawdzając każdą wartość przed dzieleniem.
źródło