Rozważmy następujący kod ( p
jest typu unsigned char*
i bitmap->width
jest typu całkowitego, który jest dokładnie nieznany i zależy od wersji jakiejś zewnętrznej biblioteki, której używamy):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Czy warto to zoptymalizować [..]
Czy może zaistnieć przypadek, w którym przyniosłoby to bardziej wydajne wyniki, pisząc:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... czy jest to trywialne dla kompilatora, aby zoptymalizować?
Jaki kod uważasz za „lepszy”?
Uwaga od redaktora (Ike): dla tych, którzy zastanawiali się nad tekstem przekreślonym, pierwotne pytanie, tak jak zostało sformułowane, było niebezpiecznie bliskie terytorium poza tematem i było bardzo bliskie zamknięcia pomimo pozytywnych opinii. Te zostały usunięte. Jednak proszę nie karać tych, którzy odpowiedzieli na te dotknięte części pytania.
c++
performance
caching
optimization
strict-aliasing
Yaron Cohen-Tal
źródło
źródło
*p
jest tego samego typu, cowidth
wtedy, optymalizacja nie jest trywialna, ponieważp
może wskazywaćwidth
i modyfikować ją wewnątrz pętli.p
wskazuje na tę samą pamięć, cobitmap->width
. Dlatego nie mogę prawnie zoptymalizować pierwszego przykładu do drugiego.Odpowiedzi:
Na pierwszy rzut oka pomyślałem, że kompilator może wygenerować równoważny zestaw dla obu wersji z aktywowanymi flagami optymalizacji. Kiedy to sprawdziłem, zdziwiłem się, widząc wynik:
Źródło
unoptimized.cpp
uwaga: ten kod nie jest przeznaczony do wykonania.
Źródło
optimized.cpp
uwaga: ten kod nie jest przeznaczony do wykonania.
Kompilacja
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Montaż (unoptimized.s)
Montaż (zoptymalizowany.s)
różn
Wygenerowany zestaw dla wersji zoptymalizowanej faktycznie ładuje (
lea
)width
stałą, w przeciwieństwie do niezoptymalizowanej wersji, która obliczawidth
przesunięcie przy każdej iteracji (movq
).Kiedy będę miał czas, w końcu opublikuję jakiś punkt odniesienia na ten temat. Dobre pytanie.
źródło
const unsigned
zamiast tylkounsigned
w niezoptymalizowanym przypadku.main
do testu dla optymalizacji. Gcc celowo oznacza go jako zimny i tym samym wyłącza niektóre jego optymalizacje. Nie wiem, czy tak jest w tym przypadku, ale to ważny nawyk.bitmap
jest globalny. Wersja inna niż CSEd używa operandu pamięci tocmp
, co nie stanowi problemu dla perf w tym przypadku. Gdyby była lokalna, kompilator mógłby założyć, że inne wskaźniki nie mogą o niej „wiedzieć” i wskazywać na nią. Przechowywanie wyrażeń obejmujących wartości globalne w zmiennych temp nie jest złym pomysłem, o ile poprawia (lub nie szkodzi) czytelność lub jeśli wydajność jest krytyczna. O ile dużo się nie dzieje, tacy miejscowi mogą zwykle żyć w rejestrach i nigdy się nie rozlać.W rzeczywistości nie ma wystarczających informacji z twojego fragmentu kodu, aby być w stanie to stwierdzić, a jedyną rzeczą, o której mogę pomyśleć, jest aliasowanie. Z naszego punktu widzenia jest całkiem jasne, że nie chcesz
p
i nie chceszbitmap
wskazywać tego samego miejsca w pamięci, ale kompilator o tym nie wie i (ponieważp
jest tego typuchar*
) kompilator musi sprawić, by ten kod działał, nawet jeślip
ibitmap
nakładają się.Oznacza to w tym przypadku, że jeśli pętla zmienia się
bitmap->width
przez wskaźnikp
, to trzeba to zobaczyć przy ponownym czytaniubitmap->width
później, co z kolei oznacza, że przechowywanie jej w zmiennej lokalnej byłoby nielegalne.Biorąc to pod uwagę, uważam, że niektóre kompilatory czasami generują dwie wersje tego samego kodu (widziałem poszlaki na to, ale nigdy bezpośrednio nie szukałem informacji o tym, co kompilator robi w tym przypadku) i szybko sprawdzą, czy wskaźniki alias i uruchom szybszy kod, jeśli uzna, że jest to w porządku.
Biorąc to pod uwagę, podtrzymuję swój komentarz dotyczący prostego pomiaru wydajności obu wersji, moje pieniądze polegają na tym, że nie widzę żadnej stałej różnicy wydajności między dwiema wersjami kodu.
Moim zdaniem takie pytania są w porządku, jeśli celem jest poznanie teorii i technik optymalizacji kompilatorów, ale jest to strata czasu (bezużyteczna mikro-optymalizacja), jeśli Twoim końcowym celem jest przyspieszenie działania programu.
źródło
restrict
kwalifikator nie byłby odpowiedzią na problem aliasingu w tym przypadku?restrict
wynika , że jest w dużej mierze chybiony. MSVC jest jedynym kompilatorem, jaki widziałem, który wydaje się robić to poprawnie. ICC traci informacje o aliasowaniu przez wywołania funkcji, nawet jeśli są one wbudowane. A GCC zwykle nie uzyskuje żadnej korzyści, chyba że zadeklarujesz każdy parametr wejściowy jakorestrict
(w tymthis
dla funkcji składowych).char
aliasy wszystkich typów, więc jeśli masz znak *, musisz go użyćrestrict
na wszystkim. Lub jeśli wymusiłeś ścisłe reguły aliasingu GCC,-fno-strict-aliasing
wtedy wszystko jest uważane za możliwy alias.restrict
semantyki podobnej do typu w C ++ jest N4150 .Ok, więc zmierzyłem
GCC -O3
(używając GCC 4.9 na Linux x64).Okazuje się, że druga wersja działa 54% szybciej!
Więc myślę, że aliasing jest rzeczą, nie myślałem o tym.
[Edytować]
Próbowałem ponownie pierwszej wersji ze wszystkimi wskaźnikami zdefiniowanymi za pomocą
__restrict__
i wyniki są takie same. Dziwne ... Albo aliasing nie jest problemem, albo z jakiegoś powodu kompilator nie optymalizuje go dobrze, nawet z__restrict__
.[Edytuj 2]
Ok, myślę, że byłem w stanie udowodnić, że problemem jest aliasing. Powtórzyłem mój pierwotny test, tym razem używając tablicy zamiast wskaźnika:
I zmierzone (trzeba było użyć „-mcmodel = large”, aby to połączyć). Potem spróbowałem:
Wyniki pomiarów były takie same - wydaje się, że kompilator był w stanie sam go zoptymalizować.
Następnie wypróbowałem oryginalne kody (ze wskazówką
p
), tym razem kiedyp
jest typustd::uint16_t*
. Znowu wyniki były takie same - z powodu ścisłego aliasingu. Potem spróbowałem budować z „-fno-strict-aliasing” i ponownie zauważyłem różnicę w czasie.źródło
Inne odpowiedzi wskazywały, że wyciągnięcie operacji wskaźnika poza pętlę może zmienić zdefiniowane zachowanie z powodu reguł aliasingu, które pozwalają char na aliasowanie czegokolwiek, a zatem nie jest dopuszczalną optymalizacją dla kompilatora, mimo że w większości przypadków jest to oczywiście poprawne dla człowieka programista.
Zwrócili również uwagę, że wyprowadzenie operacji z pętli jest zwykle, ale nie zawsze, poprawą z punktu widzenia wydajności i często jest niekorzystne z punktu widzenia czytelności.
Chciałbym podkreślić, że często istnieje „trzecia droga”. Zamiast liczyć do żądanej liczby iteracji, możesz odliczyć do zera. Oznacza to, że liczba iteracji jest potrzebna tylko raz na początku pętli, nie musi być później zapisywana. Jeszcze lepiej na poziomie asemblera często eliminuje potrzebę jawnego porównania, ponieważ operacja dekrementacji zwykle ustawia flagi wskazujące, czy licznik był zerowy zarówno przed (flaga przeniesienia), jak i po (flaga zero) dekrementacji.
Zauważ, że ta wersja pętli podaje wartości x z zakresu 1..szerokość zamiast zakresu 0 .. (szerokość-1). To nie ma znaczenia w twoim przypadku, ponieważ tak naprawdę nie używasz x do niczego, ale jest to coś, o czym należy pamiętać. Jeśli chcesz pętlę odliczającą z wartościami x w zakresie 0 .. (szerokość-1), możesz to zrobić.
Możesz również pozbyć się rzutów w powyższych przykładach, jeśli chcesz, nie martwiąc się o ich wpływ na reguły porównania, ponieważ wszystko, co robisz z bitmap-> width, to przypisywanie go bezpośrednio do zmiennej.
źródło
x --> 0
, co skutkowało operatorem „downto”. Całkiem zabawne. PS Nie uważam, aby zmienna warunku końcowego była ujemna dla czytelności, w rzeczywistości może być odwrotnie.static_cast<unsigned>(bitmap->width)
i używaniewidth
zamiast tego w pętli jest w rzeczywistości poprawą czytelności, ponieważ teraz jest mniej rzeczy do przeanalizowania przez czytelnika na wiersz. Jednak opinie innych mogą się różnić.do { } while()
, ponieważ w ASM tworzysz pętle z gałęzią warunkową na końcu. Zwykłe pętlefor(){}
iwhile(){}
wymagają dodatkowych instrukcji, aby przetestować warunek pętli raz przed pętlą, jeśli kompilator nie może udowodnić, że zawsze działa co najmniej raz. Jak najbardziej, użyjfor()
lubwhile()
kiedy warto sprawdzić, czy pętla powinna choćby uruchomić się raz, czy też jest bardziej czytelna.Jedyną rzeczą, która może uniemożliwić optymalizację, jest ścisła reguła aliasingu . W skrócie :
Wyjątek dotyczy również wskaźników
unsigned
isigned
char
.Tak jest w przypadku twojego kodu: modyfikujesz,
*p
za pomocąp
którego jest anunsigned char*
, więc kompilator musi założyć, że może wskazywaćbitmap->width
. Dlatego buforowaniebitmap->width
jest nieprawidłową optymalizacją. To zachowanie zapobiegające optymalizacji jest pokazane w odpowiedzi YSC .Gdyby i tylko wtedy, gdyby
p
wskazano na inny niżchar
i innydecltype(bitmap->width)
typ, buforowanie byłoby możliwą optymalizacją.źródło
Pytanie pierwotnie zadane:
I moja odpowiedź na to pytanie (zebranie dobrej mieszanki głosów w górę i w dół ..)
Pomimo głosów przeciw (i teraz widzę problem z aliasami), nadal jestem zadowolony z tego jako ważnej odpowiedzi. Jeśli nie wiesz, czy warto coś zoptymalizować, prawdopodobnie tak nie jest.
Oczywiście inne pytanie brzmiałoby:
Po pierwsze, czy Twoja aplikacja lub biblioteka musi działać szybciej niż obecnie? Czy użytkownik czekał zbyt długo? Czy Twoje oprogramowanie prognozuje wczorajszą pogodę zamiast jutrzejszej?
Tylko Ty możesz to naprawdę powiedzieć na podstawie tego, do czego służy Twoje oprogramowanie i czego oczekują Twoi użytkownicy.
Zakładając, że oprogramowanie wymaga optymalizacji, następną rzeczą do zrobienia jest rozpoczęcie pomiaru. Profilerowie powiedzą Ci, gdzie Twój kod spędza czas. Jeśli Twój fragment nie wyświetla się jako wąskie gardło, najlepiej zostaw go w spokoju. Profilerowie i inne narzędzia pomiarowe również powiedzą Ci, czy Twoje zmiany miały znaczenie. Można spędzić godziny na optymalizacji kodu, ale okazuje się, że nie dokonałeś żadnej zauważalnej różnicy.
Jeśli nie piszesz kodu „zoptymalizowanego”, powinien on być tak przejrzysty, czysty i zwięzły, jak tylko możesz. Argument „Przedwczesna optymalizacja jest zła” nie jest usprawiedliwieniem dla niechlujnego lub nieefektywnego kodu.
Zoptymalizowany kod zwykle poświęca niektóre z powyższych atrybutów na rzecz wydajności. Może to obejmować wprowadzenie dodatkowych zmiennych lokalnych, posiadanie obiektów o szerszym niż oczekiwany zakresie, a nawet odwrócenie kolejności normalnej pętli. Wszystko to może być mniej jasne lub zwięzłe, więc udokumentuj kod (pokrótce!), Dlaczego to robisz.
Ale często przy „wolnym” kodzie te mikro-optymalizacje są ostatnią deską ratunku. W pierwszej kolejności należy przyjrzeć się algorytmom i strukturom danych. Czy istnieje sposób, aby w ogóle uniknąć wykonywania pracy? Czy wyszukiwania liniowe można zastąpić wyszukiwaniami binarnymi? Czy lista połączona byłaby tutaj szybsza niż wektor? Albo tablica haszująca? Czy mogę zapisać wyniki w pamięci podręcznej? Podejmowanie dobrych „efektywnych” decyzji w tym miejscu często może wpłynąć na wydajność o rząd wielkości lub więcej!
źródło
W takiej sytuacji używam następującego schematu. Jest prawie tak krótki, jak twój pierwszy przypadek i jest lepszy niż drugi, ponieważ utrzymuje tymczasową zmienną lokalną dla pętli.
Będzie to szybsze z mniej niż inteligentnym kompilatorem, kompilacją do debugowania lub niektórymi flagami kompilacji.
Edycja1 : Umieszczenie stałej operacji poza pętlą jest dobrym wzorcem programowania. Pokazuje zrozumienie podstaw obsługi maszyn, szczególnie w C / C ++. Twierdzę, że wysiłek, aby udowodnić swoją wartość, powinien spoczywać na osobach, które nie przestrzegają tej praktyki. Jeśli kompilator karze za dobry wzorzec, jest to błąd w kompilatorze.
Edit2:: Zmierzyłem moją sugestię z oryginalnym kodem w vs2013, uzyskałem% 1 poprawę. Czy możemy zrobić lepiej? Prosta ręczna optymalizacja daje 3-krotną poprawę w stosunku do oryginalnej pętli na komputerze x64 bez uciekania się do egzotycznych instrukcji. Poniższy kod zakłada system little endian i odpowiednio wyrównaną mapę bitową. TEST 0 jest oryginalny (9 sekund), TEST 1 jest szybszy (3 sekundy). Założę się, że ktoś mógłby to zrobić jeszcze szybciej, a wynik testu zależałby od rozmiaru mapy bitowej. Zdecydowanie niedługo w przyszłości kompilator będzie w stanie generować konsekwentnie najszybszy kod. Obawiam się, że to będzie przyszłość, kiedy kompilator będzie również programistą AI, więc nie mielibyśmy pracy. Ale na razie po prostu napisz kod, który pokaże, że wiesz, że dodatkowa operacja w pętli nie jest potrzebna.
źródło
Należy wziąć pod uwagę dwie kwestie.
A) Jak często będzie przebiegać optymalizacja?
Jeśli odpowiedź nie pojawia się zbyt często, na przykład tylko wtedy, gdy użytkownik kliknie przycisk, nie przejmuj się, jeśli spowoduje to, że Twój kod będzie nieczytelny. Jeśli odpowiedź brzmi 1000 razy na sekundę, prawdopodobnie będziesz chciał przejść do optymalizacji. Jeśli jest to choćby trochę skomplikowane, umieść komentarz, aby wyjaśnić, co się dzieje, aby pomóc następnemu facetowi, który się pojawi.
B) Czy spowoduje to, że kod będzie trudniejszy do utrzymania / rozwiązywania problemów?
Jeśli nie widzisz dużego wzrostu wydajności, uczynienie kodu tajemniczym po prostu po to, aby zaoszczędzić kilka tyknięć zegara, nie jest dobrym pomysłem. Wiele osób powie Ci, że każdy dobry programista powinien być w stanie spojrzeć na kod i dowiedzieć się, co się dzieje. To prawda. Problem polega na tym, że w świecie biznesu dodatkowy czas na zastanowienie się nad tym kosztuje. Więc jeśli możesz sprawić, że czytanie będzie ładniejsze, zrób to. Twoi przyjaciele będą Ci za to wdzięczni.
To powiedziawszy, że osobiście użyję przykładu B.
źródło
Kompilator jest w stanie zoptymalizować wiele rzeczy. Na przykład powinieneś postawić na czytelność, łatwość obsługi i to, co jest zgodne ze standardem Twojego kodu. Aby uzyskać więcej informacji o tym, co można zoptymalizować (za pomocą GCC), zobacz ten wpis na blogu .
źródło
Z reguły pozwól kompilatorowi przeprowadzić optymalizację za Ciebie, dopóki nie zdecydujesz, że powinieneś przejąć kontrolę. Logika tego nie ma nic wspólnego z wydajnością, ale raczej z czytelnością dla człowieka. W zdecydowanej większości przypadków czytelność programu jest ważniejsza niż jego wydajność. Powinieneś dążyć do pisania kodu, który jest łatwiejszy do odczytania dla człowieka, a następnie martwić się o optymalizację tylko wtedy, gdy jesteś przekonany, że wydajność jest ważniejsza niż łatwość utrzymania kodu.
Gdy zobaczysz, że wydajność ma znaczenie, należy uruchomić profiler w kodzie, aby określić, które pętle są nieefektywne, i zoptymalizować je indywidualnie. Rzeczywiście mogą istnieć przypadki, w których chcesz przeprowadzić tę optymalizację (zwłaszcza jeśli migrujesz do C ++, gdzie zaangażowane są kontenery STL), ale koszt w zakresie czytelności jest duży.
Ponadto przychodzą mi do głowy sytuacje patologiczne, w których mogłoby to faktycznie spowolnić kod. Na przykład rozważmy przypadek, w którym kompilator nie mógł udowodnić, że
bitmap->width
był on stały w trakcie procesu. Dodającwidth
zmienną, wymuszasz na kompilatorze utrzymywanie zmiennej lokalnej w tym zakresie. Jeśli z jakiegoś powodu, specyficznego dla platformy, ta dodatkowa zmienna uniemożliwiła pewną optymalizację miejsca na stosie, może być konieczna reorganizacja sposobu, w jaki emituje kody bajtowe, i wyprodukowanie czegoś mniej wydajnego.Na przykład w systemie Windows x64 należy wywołać specjalne wywołanie API
__chkstk
w preambule funkcji, jeśli funkcja będzie wykorzystywać więcej niż 1 stronę zmiennych lokalnych. Ta funkcja daje systemowi Windows możliwość zarządzania stronami ochronnymi, których używają do rozszerzania stosu w razie potrzeby. Jeśli twoja dodatkowa zmienna przesuwa użycie stosu z poziomu poniżej 1 strony na 1 stronę lub powyżej, twoja funkcja jest teraz zobowiązana do wywoływania za__chkstk
każdym razem, gdy jest wprowadzana. Gdybyś miał zoptymalizować tę pętlę na wolnej ścieżce, możesz w rzeczywistości spowolnić szybką ścieżkę bardziej niż zaoszczędziłeś na wolnej ścieżce!Jasne, jest to trochę patologiczne, ale celem tego przykładu jest to, że możesz faktycznie spowolnić kompilator. Pokazuje tylko, że musisz profilować swoją pracę, aby określić, gdzie idą optymalizacje. W międzyczasie prosimy nie poświęcać w żaden sposób czytelności na rzecz optymalizacji, która może mieć znaczenie lub nie.
źródło
Porównanie jest błędne , ponieważ dwóch fragmentów kodu
i
nie są równoważne
W pierwszym przypadku
width
jest zależna, a nie stała, i nie można zakładać, że może się ona nie zmieniać między kolejnymi iteracjami. Dlatego nie można go zoptymalizować, ale należy go sprawdzić w każdej pętli .W zoptymalizowanym przypadku zmiennej lokalnej przypisywana jest wartość
bitmap->width
w pewnym momencie podczas wykonywania programu. Kompilator może sprawdzić, czy to się nie zmienia.Czy myślałeś o wielowątkowości, czy może wartość może być zewnętrznie zależna, tak że jej wartość jest zmienna. Jak można by oczekiwać, że kompilator rozwiąże wszystkie te kwestie, jeśli nie powiesz?
Kompilator może działać tylko na tyle dobrze, na ile pozwala na to Twój kod.
źródło
O ile nie wiesz, jak dokładnie kompilator optymalizuje kod, lepiej jest przeprowadzić własne optymalizacje, zachowując czytelność kodu i projekt. Praktycznie trudno jest sprawdzić kod asemblera dla każdej funkcji, którą piszemy dla nowych wersji kompilatora.
źródło
Kompilator nie może zoptymalizować,
bitmap->width
ponieważwidth
można zmienić wartość między iteracjami. Istnieje kilka najczęstszych powodów:iterator::end()
czycontainer::size()
tak trudno jest przewidzieć, czy będzie on zawsze zwraca ten sam wynik.Reasumując (moja osobista opinia) dla miejsc wymagających wysokiego poziomu optymalizacji trzeba to zrobić samemu, w innych miejscach po prostu to zostaw, kompilator może to zoptymalizować lub nie, jeśli nie ma dużej różnicy czytelność kodu jest głównym celem.
źródło