Jestem osobą religijną i staram się nie popełniać grzechów. Dlatego mam tendencję do pisania małych ( mniejszych , aby przeformułować Roberta C. Martina) funkcji, aby zachować zgodność z kilkoma przykazaniami nakazanymi przez Biblię Czystego Kodu . Ale sprawdzając niektóre rzeczy, wylądowałem na tym poście , poniżej którego przeczytałem ten komentarz:
Pamiętaj, że koszt wywołania metody może być znaczny, w zależności od języka. Prawie zawsze istnieje kompromis między pisaniem czytelnego kodu a pisaniem kodu wykonawczego.
W jakich warunkach to cytowane stwierdzenie jest nadal aktualne, biorąc pod uwagę bogatą branżę wydajnych nowoczesnych kompilatorów?
To jest moje jedyne pytanie. I nie chodzi o to, czy powinienem pisać długie czy małe funkcje. Podkreślam tylko, że wasze opinie mogą - lub nie - przyczynić się do zmiany mojego nastawienia i uniemożliwić mi oparcie się pokusie bluźnierców .
źródło
for(Integer index = 0, size = someList.size(); index < size; index++)
zamiast po prostufor(Integer index = 0; index < someList.size(); index++)
. To, że Twój kompilator powstał w ciągu ostatnich kilku lat, niekoniecznie oznacza, że możesz zrezygnować z profilowania.main()
, inni dzielą wszystko na około 50 małych funkcji i wszystkie są całkowicie nieczytelne. Sztuką jest, jak zawsze, zachowanie równowagi .Odpowiedzi:
To zależy od twojej domeny.
Jeśli piszesz kod mikrokontrolera o niskiej mocy, koszt wywołania metody może być znaczny. Ale jeśli tworzysz normalną stronę internetową lub aplikację, koszt wywołania metody będzie nieznaczny w porównaniu z resztą kodu. W takim przypadku zawsze warto skupić się na właściwych algorytmach i strukturach danych zamiast na mikrooptymalizacjach, takich jak wywołania metod.
Jest też kwestia kompilatora wprowadzającego metody dla ciebie. Większość kompilatorów jest wystarczająco inteligentna, aby wstawiać funkcje tam, gdzie jest to możliwe.
I wreszcie, złota zasada: ZAWSZE PROFIL ZAWSZE PIERWSZY. Nie pisz „zoptymalizowanego” kodu w oparciu o założenia. Jeśli nie masz pewności, napisz oba przypadki i sprawdź, co jest lepsze.
źródło
Narzut wywołania funkcji zależy całkowicie od języka i od poziomu, który optymalizujesz.
Na bardzo niskim poziomie wywołania funkcji, a nawet więcej, dlatego wirtualne wywołania metod mogą być kosztowne, jeśli prowadzą do nieprzewidywalności gałęzi lub błędów pamięci podręcznej procesora. Jeśli napisałeś asembler , będziesz również wiedział, że potrzebujesz kilku dodatkowych instrukcji, aby zapisać i przywrócić rejestry wokół rozmowy. Nie jest prawdą, że „wystarczająco inteligentny” kompilator byłby w stanie wprowadzić właściwe funkcje, aby uniknąć tego narzutu, ponieważ kompilatory są ograniczone semantyką języka (szczególnie wokół takich funkcji, jak wysyłanie metod interfejsu lub dynamicznie ładowane biblioteki).
Na wysokim poziomie języki takie jak Perl, Python, Ruby wykonują wiele operacji księgowych na wywołanie funkcji, co czyni je stosunkowo kosztownymi. Sytuację pogarsza metaprogramowanie. Kiedyś przyspieszyłem oprogramowanie Python 3x po prostu podnosząc wywołania funkcji z bardzo gorącej pętli. W kodzie krytycznym dla wydajności wbudowane funkcje pomocnicze mogą mieć zauważalny efekt.
Jednak zdecydowana większość oprogramowania nie jest tak bardzo krytyczna pod względem wydajności, że można zauważyć ogólne wywołanie funkcji. W każdym razie pisanie czystego, prostego kodu się opłaca:
Jeśli kod nie ma krytycznego wpływu na wydajność, ułatwia to konserwację. Nawet w oprogramowaniu krytycznym pod względem wydajności większość kodu nie będzie „gorącym punktem”.
Jeśli kod ma krytyczny wpływ na wydajność, prosty kod ułatwia zrozumienie kodu i dostrzega możliwości optymalizacji. Największe wygrane zwykle nie pochodzą z mikrooptymalizacji, takich jak funkcje wstawiania, ale z ulepszeń algorytmicznych. Lub inaczej: nie rób tego samego szybciej. Znajdź sposób na zrobienie mniej.
Zauważ, że „prosty kod” nie oznacza „podzielony na tysiąc drobnych funkcji”. Każda funkcja wprowadza również trochę narzutu poznawczego - trudniej jest uzasadnić bardziej abstrakcyjny kod. W pewnym momencie te małe funkcje mogą zrobić tak niewiele, że ich nie użycie uprości twój kod.
źródło
Prawie wszystkie reklamy dotyczące strojenia kodu pod kątem wydajności są specjalnymi przypadkami prawa Amdahla . Krótkie, humorystyczne stwierdzenie prawa Amdahla brzmi:
(Optymalizacja rzeczy do zera w czasie wykonywania jest całkowicie możliwa: kiedy usiądziesz, aby zoptymalizować duży, skomplikowany program, prawdopodobnie zauważysz, że wydaje on przynajmniej część swojego czasu pracy na rzeczy, których wcale nie musi robić .)
Dlatego ludzie zwykle mówią, aby nie martwić się kosztami wywołań funkcji: bez względu na to, jak są drogie, zwykle program jako całość spędza tylko niewielką część czasu wykonywania na kosztach połączeń, więc przyspieszenie ich nie pomaga bardzo .
Ale jeśli istnieje sztuczka, którą można wyciągnąć, która przyspiesza wszystkie wywołania funkcji, taka sztuczka jest prawdopodobnie tego warta. Deweloperzy kompilatorów spędzają mnóstwo czasu na optymalizacji funkcji „prologów” i „epilogów”, ponieważ przynosi to korzyści wszystkim programom kompilowanym z tym kompilatorem, nawet jeśli jest to tylko odrobina dla każdego.
A jeśli mają powody, aby sądzić, że program jest spędzać dużo jej wykonywania tylko wywołań funkcji, to należy zacząć myśleć o tym, czy niektóre z tych wywołań funkcji są niepotrzebne. Oto kilka praktycznych zasad określających, kiedy należy to zrobić:
Jeśli czas działania funkcji dla pojedynczego wywołania jest krótszy niż milisekunda, ale funkcja ta jest wywoływana setki tysięcy razy, prawdopodobnie należy ją wstawić.
Jeśli profil programu pokazuje tysiące funkcji i żadna z nich nie zajmuje więcej niż 0,1% czasu wykonywania, wówczas narzut wywołania funkcji jest prawdopodobnie znaczący łącznie.
Jeśli masz „ kod lasagna ”, w którym istnieje wiele warstw abstrakcji, które nie wykonują prawie żadnej pracy poza przeniesieniem do następnej warstwy, a wszystkie te warstwy są implementowane za pomocą wirtualnych wywołań metod, istnieje duża szansa, że procesor zmarnuje dużo czasu na pośrednich przeciągnięciach rurociągów. Niestety, jedynym lekarstwem na to jest pozbycie się niektórych warstw, co często jest bardzo trudne.
źródło
final
klas i metod, tam gdzie ma to zastosowanie w Javie, lub nievirtual
metod w C # lub C ++), wówczas kompilator / środowisko wykonawcze może wyeliminować pośrednie działanie, a Ty ' Zobaczę zysk bez ogromnej restrukturyzacji. Jak wskazuje @JorgWMittag, JVM może nawet inline inline w przypadkach, w których nie można udowodnić, że optymalizacja jest ...Podważę ten cytat:
Jest to bardzo mylące stwierdzenie i potencjalnie niebezpieczne podejście. Istnieją pewne szczególne przypadki, w których musisz dokonać kompromisu, ale ogólnie dwa czynniki są niezależne.
Przykładem koniecznego kompromisu jest prosty algorytm w porównaniu z bardziej złożonym, ale bardziej wydajnym. Implementacja hashtable jest wyraźnie bardziej złożona niż implementacja listy połączonej, ale wyszukiwanie będzie wolniejsze, więc może być konieczne wymienienie prostoty (która jest czynnikiem wpływającym na czytelność) na wydajność.
Jeśli chodzi o narzut wywołania funkcji, przekształcenie algorytmu rekurencyjnego w iteracyjny może mieć znaczącą korzyść w zależności od algorytmu i języka. Ale jest to ponownie bardzo specyficzny scenariusz i ogólnie narzut wywołania funkcji będzie znikomy lub zoptymalizowany.
(Niektóre dynamiczne języki, takie jak Python, mają znaczny narzut wywołania metod. Ale jeśli wydajność staje się problemem, prawdopodobnie nie powinieneś używać Pythona.)
Większość zasad dotyczących czytelnego kodu - spójne formatowanie, znaczące nazwy identyfikatorów, odpowiednie i pomocne komentarze itd. Nie mają wpływu na wydajność. A niektóre - na przykład używanie wyliczeń zamiast ciągów - mają również zalety w zakresie wydajności.
źródło
Narzut wywołania funkcji jest w większości przypadków nieistotny.
Jednak większy zysk z wstawiania kodu polega na optymalizacji nowego kodu po wstawieniu .
Na przykład, jeśli wywołasz funkcję ze stałym argumentem, optymalizator może teraz stale składać ten argument tam, gdzie nie mógł, zanim wstawi wywołanie. Jeśli argument jest wskaźnikiem funkcji (lub lambda), optymalizator może teraz również wywoływać wywołania tej lambdy.
Jest to duży powód, dla którego funkcje wirtualne i wskaźniki funkcji nie są atrakcyjne, ponieważ nie można ich w ogóle wstawić, chyba że rzeczywisty wskaźnik funkcji jest stale składany aż do strony wywołania.
źródło
Zakładając, że wydajność ma znaczenie dla twojego programu i rzeczywiście ma wiele połączeń, koszt nadal może, ale nie musi mieć znaczenia, w zależności od rodzaju połączenia.
Jeśli wywoływana funkcja jest mała, a kompilator jest w stanie ją wstawić, wówczas koszt będzie zasadniczo zerowy. Nowoczesne kompilatory / implementacje językowe mają JIT, optymalizacje czasu łącza i / lub systemy modułowe zaprojektowane w celu maksymalizacji możliwości wstawiania funkcji, gdy jest to korzystne.
OTOH, istnieje nieoczywisty koszt wywoływania funkcji: ich samo istnienie może hamować optymalizacje kompilatora przed i po wywołaniu.
Jeśli kompilator nie może zrozumieć, co robi wywoływana funkcja (np. Wirtualne / dynamiczne wysyłanie lub funkcja w bibliotece dynamicznej), może być konieczne pesymistyczne założenie, że funkcja może mieć jakikolwiek efekt uboczny - wyrzucić wyjątek, zmodyfikować stan globalny lub zmień dowolną pamięć widzianą przez wskaźniki. Kompilator może być zmuszony zapisać wartości tymczasowe do pamięci zapasowej i ponownie odczytać je po wywołaniu. Nie będzie w stanie zmienić kolejności instrukcji wokół połączenia, więc może nie być w stanie wektoryzować pętli lub wyciągać zbędne obliczenia z pętli.
Na przykład, jeśli niepotrzebnie wywołujesz funkcję w każdej iteracji pętli:
Kompilator może wiedzieć, że jest to czysta funkcja i przenieść ją poza pętlę (w strasznym przypadku, takim jak ten przykład, nawet naprawia przypadkowy algorytm O (n ^ 2) na O (n)):
A może nawet przepisać pętlę do przetwarzania elementów 4/8/16 jednocześnie za pomocą instrukcji wide / SIMD.
Ale jeśli dodasz wywołanie do jakiegoś nieprzezroczystego kodu w pętli, nawet jeśli wywołanie nic nie robi i samo jest super tanie, kompilator musi przyjąć najgorsze - że wywołanie uzyska dostęp do zmiennej globalnej, która wskazuje na tę samą pamięć jak
s
zmiana jego zawartość (nawet jeśli jestconst
w twojej funkcji, może byćconst
nigdzie indziej), co uniemożliwia optymalizację:źródło
Ten stary artykuł może odpowiedzieć na twoje pytanie:
Abstrakcyjny:
źródło
W C ++ uważaj na projektowanie wywołań funkcji kopiujących argumenty, domyślnie jest to „pass by value”. Narzut wywołania funkcji z powodu zapisywania rejestrów i innych rzeczy związanych z ramkami stosu może zostać przytłoczony przez niezamierzoną (i potencjalnie bardzo kosztowną) kopię obiektu.
Istnieją optymalizacje związane z ramkami stosu, które należy zbadać przed rezygnacją z wysoce faktoryzowanego kodu.
Przez większość czasu, kiedy miałem do czynienia z wolnym programem, stwierdziłem, że wprowadzanie zmian algorytmicznych przyniosło znacznie większe przyspieszenie niż wywołania funkcji wstawiania. Na przykład: inny inżynier przerobił analizator składni, który wypełnił strukturę map-of-map. W ramach tego usunął indeks z pamięci podręcznej z jednej mapy do logicznie powiązanej. To był niezły ruch w zakresie odporności kodu, jednak sprawił, że program był bezużyteczny z powodu 100-krotnego spowolnienia ze względu na wykonanie wyszukiwania skrótu dla wszystkich przyszłych dostępów w porównaniu z użyciem przechowywanego indeksu. Profilowanie wykazało, że większość czasu poświęcono na funkcję haszującą.
źródło
Jak mówią inni, najpierw powinieneś zmierzyć wydajność swojego programu i prawdopodobnie nie znajdziesz żadnej różnicy w praktyce.
Mimo to, z poziomu koncepcyjnego, myślałem, że wyjaśnię kilka rzeczy, które są powiązane z twoim pytaniem. Po pierwsze pytasz:
Zwróć uwagę na słowa kluczowe „funkcja” i „kompilatory”. Twój cytat jest subtelnie inny:
Mówi się o metodach w sensie obiektowym.
Podczas gdy „funkcja” i „metoda” są często używane zamiennie, istnieją różnice, jeśli chodzi o ich koszt (o który pytasz) i jeśli chodzi o kompilację (który jest kontekstem, który podałeś).
W szczególności musimy wiedzieć o wysyłce statycznej a dynamicznej . Na razie zignoruję optymalizacje.
W języku takim jak C zwykle wywołujemy funkcje z wysyłaniem statycznym . Na przykład:
Gdy kompilator widzi wywołanie
foo(y)
, wie, do jakiej funkcjifoo
odnosi się nazwa, więc program wyjściowy może przejść bezpośrednio dofoo
funkcji, co jest dość tanie. To właśnie oznacza wysyłkę statyczną .Alternatywą jest dynamiczne wysyłanie , w którym kompilator nie wie, która funkcja jest wywoływana. Jako przykład podajemy kod Haskell (ponieważ odpowiednik C byłby niechlujny!):
Tutaj
bar
funkcja wywołuje swój argumentf
, którym może być cokolwiek. Stąd kompilator nie może po prostu skompilowaćbar
do instrukcji szybkiego skoku, ponieważ nie wie, do którego skoku. Zamiast tego generowany przez nas kod niebar
będzie ustalał,f
do której funkcji wskazuje, a następnie przeskoczy do niej. To właśnie oznacza dynamiczna wysyłka .Oba te przykłady dotyczą funkcji . Wspomniałeś o metodach , które można traktować jako szczególny styl dynamicznie wywoływanej funkcji. Na przykład, oto niektóre Python:
y.foo()
Wezwanie wykorzystuje dynamiczne wysyłkę, ponieważ patrzy się wartośćfoo
właściwości wy
obiekcie, a nazywając cokolwiek znajdzie; nie wie, żey
będzie miała klasęA
lub żeA
klasa zawierafoo
metodę, więc nie możemy po prostu przejść do niej od razu.OK, to podstawowy pomysł. Pamiętaj, że wysyłka statyczna jest szybsza niż wysyłka dynamiczna, niezależnie od tego, czy kompilujemy, czy interpretujemy; wszystko inne jest równe. Dereferencing wiąże się z dodatkowymi kosztami.
Jak to wpływa na nowoczesne, optymalizujące kompilatory?
Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że statyczne wysyłanie można zoptymalizować bardziej: gdy wiemy, do której funkcji przeskakujemy, możemy wykonywać takie czynności jak wstawianie. Dzięki dynamicznej wysyłce nie wiemy, że skaczemy do czasu wykonania, więc nie możemy wiele zoptymalizować.
Po drugie, w niektórych językach można wywnioskować, gdzie niektóre dynamiczne wysyłki zakończą przeskakiwanie, a tym samym zoptymalizować je do wysyłki statycznej. Dzięki temu możemy przeprowadzać inne optymalizacje, takie jak wstawianie itp.
W powyższym przykładzie Python takie wnioskowanie jest dość beznadziejne, ponieważ Python pozwala innym kodom na przesłonięcie klas i właściwości, więc trudno jest wywnioskować wiele, które będą obowiązywać we wszystkich przypadkach.
Jeśli nasz język pozwala nam nałożyć więcej ograniczeń, na przykład ograniczając się
y
do klasyA
za pomocą adnotacji, moglibyśmy wykorzystać te informacje do wnioskowania o funkcji docelowej. W językach z podklasą (czyli prawie wszystkie języki z klasami!) To w rzeczywistości za mało, ponieważy
może mieć inną (pod) klasę, więc potrzebowalibyśmy dodatkowych informacji, takich jakfinal
adnotacje Javy, aby dokładnie wiedzieć, która funkcja zostanie wywołana.Haskell nie jest językiem OO, ale możemy wywnioskować wartość
f
przez inlinebar
(co jest statycznie wysłane) domain
podstawiającfoo
zay
. Ponieważ cel parametrufoo
inmain
jest statycznie znany, wywołanie jest statycznie wysyłane i prawdopodobnie zostanie wbudowane i całkowicie zoptymalizowane (ponieważ te funkcje są małe, kompilator jest bardziej skłonny je wbudować; chociaż nie możemy na to liczyć ).Stąd koszt sprowadza się do:
Jeśli używasz „bardzo dynamicznego” języka, z dużą ilością dynamicznej wysyłki i kilkoma gwarancjami dostępnymi dla kompilatora, każde połączenie będzie wiązało się z kosztami. Jeśli używasz „bardzo statycznego” języka, dojrzały kompilator wygeneruje bardzo szybki kod. Jeśli jesteś pomiędzy, może to zależeć od twojego stylu kodowania i tego, jak inteligentna jest implementacja.
źródło
Tak, przewidywane pominięcie gałęzi jest bardziej kosztowne na nowoczesnym sprzęcie niż jeszcze kilkadziesiąt lat temu, ale kompilatory są znacznie mądrzejsze w optymalizacji tego.
Jako przykład rozważmy Javę. Na pierwszy rzut oka narzut powinien być szczególnie dominujący w tym języku:
Przerażony tymi praktykami przeciętny programista C przewidziałby, że Java musi być co najmniej o jeden rząd wielkości wolniejsza niż C. A 20 lat temu miałby rację. Nowoczesne testy porównawcze umieszczają jednak idiomatyczny kod Java w granicach kilku procent równoważnego kodu C. Jak to możliwe?
Jednym z powodów jest to, że nowoczesne funkcje wbudowane JVM są oczywiście wywoływane. Czyni to za pomocą inkluzywnego wstawiania:
To znaczy kod:
zostaje przepisany na
I oczywiście środowisko wykonawcze jest wystarczająco inteligentne, aby przejść w górę tego sprawdzania typu, dopóki punkt nie jest przypisany, lub pomijać go, jeśli typ jest znany kodowi wywołującemu.
Podsumowując, jeśli nawet Java zarządza automatycznym wstawianiem metod, nie ma nieodłącznego powodu, dla którego kompilator nie może obsługiwać automatycznego wstawiania, i każdy powód, aby to robić, ponieważ wstawianie jest bardzo korzystne dla nowoczesnych procesorów. Dlatego nie mogę sobie wyobrazić żadnego nowoczesnego kompilatora głównego nurtu, nieświadomego tej najbardziej podstawowej strategii optymalizacji, i założyłbym, że jest to kompilator, o ile nie zostanie udowodnione inaczej.
źródło
Jest to niestety wysoce zależne od:
Po pierwsze, pierwszą zasadą optymalizacji wydajności jest profil pierwszy . Istnieje wiele domen, w których wydajność części oprogramowania jest nieistotna dla wydajności całego stosu: wywołania bazy danych, operacje sieciowe, operacje systemu operacyjnego, ...
Oznacza to, że wydajność oprogramowania jest całkowicie nieistotna, nawet jeśli nie poprawia opóźnień, optymalizacja oprogramowania może przynieść oszczędności energii i sprzętu (lub oszczędności baterii w aplikacjach mobilnych), co może mieć znaczenie.
Jednak zazwyczaj NIE można ich oczarować i często ulepszenia algorytmów przebijają mikrooptymalizacje z dużym marginesem.
Tak więc przed optymalizacją musisz zrozumieć, dla czego optymalizujesz ... i czy warto.
Teraz, jeśli chodzi o czystą wydajność oprogramowania, różni się znacznie między łańcuchami narzędzi.
Wywołanie funkcji wiąże się z dwoma kosztami:
Koszt czasu działania jest raczej oczywisty; w celu wykonania wywołania funkcji konieczna jest pewna ilość pracy. Na przykład przy użyciu C na x86 wywołanie funkcji będzie wymagało (1) rozlewania rejestrów na stos, (2) wypychania argumentów do rejestrów, wykonywania wywołania, a następnie (3) przywracania rejestrów ze stosu. Zobacz to podsumowanie konwencji wywoływania, aby zobaczyć zaangażowaną pracę .
Rozlewanie / przywracanie rejestru zajmuje niebanalną ilość razy (kilkadziesiąt cykli procesora).
Ogólnie oczekuje się, że koszt ten będzie trywialny w porównaniu z faktycznym kosztem wykonania funkcji, jednak niektóre wzorce przynoszą efekt przeciwny do zamierzonego: funkcje pobierające, funkcje chronione przez prosty warunek itp.
Prócz interpretatorów programista będzie miał zatem nadzieję, że ich kompilator lub JIT zoptymalizują niepotrzebne wywołania funkcji; chociaż ta nadzieja może czasami nie przynieść owoców. Ponieważ optymalizatory nie są magią.
Optymalizator może wykryć, że wywołanie funkcji jest trywialne, a inline połączenia: w zasadzie, kopiowanie / wklejanie ciało funkcji w miejscu wywołania. Nie zawsze jest to dobra optymalizacja (może wywoływać wzdęcia), ale ogólnie jest opłacalna, ponieważ wstawianie odsłania kontekst , a kontekst umożliwia więcej optymalizacji.
Typowym przykładem jest:
Jeśli
func
zostanie wstawiony, optymalizator zda sobie sprawę, że gałąź nigdy nie zostanie przejęta i zoptymalizujecall
jąvoid call() {}
.W tym sensie wywołania funkcji, ukrywając informacje przed optymalizatorem (jeśli jeszcze nie zostały wstawione), mogą blokować niektóre optymalizacje. Wirtualne wywołania funkcji są szczególnie winne, ponieważ dewirtualizacja (dowodzenie, która funkcja ostatecznie zostanie wywołana w czasie wykonywania) nie zawsze jest łatwa.
Podsumowując, radzę najpierw napisać wyraźnie , unikając przedwczesnej pesymizacji algorytmicznej (złożoność sześcienna lub gorsze brania szybko), a następnie zoptymalizuj tylko to, co wymaga optymalizacji.
źródło
Będę stanowczo powiedzieć, że nigdy. Uważam, że cytat jest nierozważny, by po prostu tam rzucić.
Oczywiście nie mówię pełnej prawdy, ale nie dbam o to, aby być tak szczerym. To tak, jak w tym filmie Matrix, zapomniałem, czy to był 1, 2 czy 3 - myślę, że to ten z seksowną włoską aktorką z dużymi melonami (tak naprawdę nie lubiłem żadnego oprócz pierwszego), kiedy Pani wyroczni powiedziała Keanu Reevesowi: „Właśnie powiedziałam ci, co musisz usłyszeć” lub coś w tym rodzaju, właśnie to chcę teraz zrobić.
Programiści nie muszą tego słyszeć. Jeśli mają doświadczenie z profilerami w ręku, a cytat jest w pewnym stopniu odpowiedni dla ich kompilatorów, to już to wiedzą i nauczą się tego we właściwy sposób, pod warunkiem, że rozumieją swoje wyniki profilowania i dlaczego niektóre wywołania liści są hotspotami poprzez pomiar. Jeśli nie mają doświadczenia i nigdy nie profilowali swojego kodu, jest to ostatnia rzecz, którą muszą usłyszeć, że powinni zacząć zabobonnie kompromitować sposób pisania kodu do punktu wstawiania wszystkiego, zanim nawet zidentyfikują punkty aktywne w nadziei, że to zrobi stać się bardziej wydajnym.
Tak czy inaczej, zależy to od dokładniejszej odpowiedzi. Niektóre z wielu warunków są już wymienione wśród dobrych odpowiedzi. Możliwe warunki wyboru jednego języka są już same w sobie ogromne, takie jak C ++, który musiałby zostać dynamicznie wysłany w rozmowach wirtualnych i kiedy można go zoptymalizować i pod którym kompilatory, a nawet linkery, i to już gwarantuje szczegółową odpowiedź, nie mówiąc już o próbie aby sprostać warunkom w każdym możliwym języku i kompilatorze. Ale dodam na górze: „kogo to obchodzi?” ponieważ nawet pracując w obszarach krytycznych pod względem wydajności, takich jak raytracing, ostatnią rzeczą, którą zacznę robić od początku, są metody ręcznego wprowadzania, zanim będę miał jakiekolwiek pomiary.
Uważam, że niektórzy ludzie nadgorliwie sugerują, aby nigdy nie dokonywać żadnych mikrooptymalizacji przed pomiarem. Jeśli optymalizacja pod kątem lokalizacji odniesień liczy się jako mikrooptymalizacja, to często zaczynam stosować takie optymalizacje od samego początku z podejściem do projektowania zorientowanym na dane w obszarach, które z pewnością będą kluczowe dla wydajności (np. Kod raytracing), bo inaczej wiem, że będę musiał przepisać duże sekcje wkrótce po pracy w tych domenach przez lata. Optymalizacja reprezentacji danych dla trafień w pamięci podręcznej może często mieć ten sam rodzaj poprawy wydajności co ulepszenia algorytmu, chyba że mówimy o czasie kwadratowym do liniowego.
Ale nigdy nie widzę dobrego powodu, aby rozpocząć wstawianie przed pomiarami, zwłaszcza, że profilerzy są w stanie ujawnić, co może skorzystać z wstawiania, ale nie ujawnić, co może zyskać na braku wstawiania (a brak wstawiania może w rzeczywistości przyspieszyć kod, jeśli Wywołanie funkcji bez podszewki jest rzadkim przypadkiem, poprawiając lokalizację odniesienia dla icache dla kodu gorącego, a czasem nawet pozwalając optymalizatorom wykonać lepszą pracę dla wspólnej ścieżki wykonywania instrukcji).
źródło