Jest to sytuacja, z którą często się spotykam jako niedoświadczony programista i zastanawiam się nad tym, szczególnie w przypadku mojego ambitnego, intensywnego projektu, który staram się zoptymalizować. W przypadku głównych języków podobnych do języka C (C, objC, C ++, Java, C # itp.) I ich zwykłych kompilatorów, czy te dwie funkcje będą działać równie wydajnie? Czy jest jakaś różnica w skompilowanym kodzie?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Zasadniczo, czy istnieje bezpośredni bonus / kara za wydajność podczas wczesnego break
robienia lub return
robienia zdjęć? W jaki sposób działa stackframe? Czy istnieją zoptymalizowane przypadki specjalne? Czy są jakieś czynniki (takie jak inlining lub rozmiar „Do rzeczy”), które mogą na to znacząco wpłynąć?
Zawsze jestem zwolennikiem lepszej czytelności w stosunku do drobnych optymalizacji (często widzę foo1 przy walidacji parametrów), ale pojawia się to tak często, że chciałbym raz na zawsze odłożyć na bok wszelkie zmartwienia.
I zdaję sobie sprawę z pułapek przedwczesnej optymalizacji ... ugh, to są bolesne wspomnienia.
EDYCJA: Przyjąłem odpowiedź, ale odpowiedź EJP dość zwięźle wyjaśnia, dlaczego użycie a return
jest praktycznie pomijalne (w asemblerze return
tworzy „gałąź” na końcu funkcji, która jest niezwykle szybka. Gałąź zmienia rejestr komputera i może również wpływać na pamięć podręczną i potok, co jest dość malutkie). W tym przypadku dosłownie nie robi to różnicy, ponieważ zarówno the, jak if/else
i the return
tworzą tę samą gałąź na końcu funkcji.
Odpowiedzi:
Nie ma żadnej różnicy:
Oznacza to, że nie ma żadnej różnicy w generowanym kodzie, nawet bez optymalizacji w dwóch kompilatorach
źródło
something()
zawsze zostanie stracony. W pierwotnym pytaniu OP maDo stuff
i wDo diffferent stuff
zależności od flagi. Nie jestem pewien, że wygenerowany kod będzie taki sam.Krótka odpowiedź brzmi: nie ma różnicy. Zrób sobie przysługę i przestań się tym martwić. Optymalizujący kompilator jest prawie zawsze mądrzejszy od Ciebie.
Skoncentruj się na czytelności i łatwości konserwacji.
Jeśli chcesz zobaczyć, co się stanie, zbuduj je z włączonymi optymalizacjami i spójrz na wyjście asemblera.
źródło
x = <some number>
niżif(<would've changed>) x = <some number>
niepotrzebne gałęzie mogą naprawdę boleć. Z drugiej strony, gdyby nie było to w głównej pętli niezwykle intensywnej pracy, też bym się tym nie martwił.Ciekawe odpowiedzi: Chociaż zgadzam się z nimi wszystkimi (na razie), istnieją możliwe konotacje tego pytania, które do tej pory są całkowicie pomijane.
Jeśli powyższy prosty przykład zostanie rozszerzony o alokację zasobów, a następnie sprawdzanie błędów z potencjalnym wynikającym z tego zwolnieniem zasobów, obraz może się zmienić.
Rozważ naiwne podejście, które mogą przyjąć początkujący:
Powyższe reprezentowałoby skrajną wersję stylu przedwczesnego powrotu. Zwróć uwagę, jak kod staje się bardzo powtarzalny i niemożliwy do utrzymania w czasie, gdy rośnie jego złożoność. W dzisiejszych czasach ludzie mogą używać obsługi wyjątków, aby je złapać.
Philip zasugerował, po obejrzeniu poniższego przykładu goto, aby użyć bezłamkowego przełącznika / obudowy wewnątrz bloku catch powyżej. Można by się przełączyć (typeof (e)), a następnie upaść przez
free_resourcex()
wywołania, ale nie jest to trywialne i wymaga rozważenia projektowego . I pamiętaj, że przełącznik / obudowa bez przerw jest dokładnie taki sam, jak goto z połączonymi łańcuchami etykietami poniżej ...Jak zauważył Mark B, w C ++ za dobry styl uważa się podążanie za zasadą pozyskiwania zasobów to inicjalizacja , w skrócie RAII . Istotą koncepcji jest użycie instancji obiektu do pozyskiwania zasobów. Zasoby są następnie automatycznie zwalniane, gdy tylko obiekty wyjdą poza zasięg i zostaną wywołane ich destruktory. W przypadku współzależnych zasobów należy zwrócić szczególną uwagę, aby zapewnić prawidłową kolejność zwalniania alokacji i zaprojektować takie typy obiektów, aby wymagane dane były dostępne dla wszystkich destruktorów.
Lub w dni przed wyjątkiem może to zrobić:
Ale ten nadmiernie uproszczony przykład ma kilka wad: Można go używać tylko wtedy, gdy przydzielone zasoby nie są od siebie zależne (np. Nie można go użyć do alokacji pamięci, a następnie otwarcia uchwytu pliku, a następnie wczytania danych z uchwytu do pamięci ) i nie dostarcza indywidualnych, rozróżnialnych kodów błędów jako wartości zwracanych.
Aby kod był szybki (!), Zwarty, czytelny i rozszerzalny, Linus Torvalds narzucił inny styl kodu jądra, który zajmuje się zasobami, nawet używając niesławnego goto w sposób, który ma absolutnie sens :
Istotą dyskusji na listach dyskusyjnych jądra jest to, że większość funkcji językowych, które są „preferowane” w stosunku do instrukcji goto, to niejawne goto, takie jak ogromne, podobne do drzewa if / else, programy obsługi wyjątków, instrukcje pętli / przerwania / kontynuacji itp. .A goto w powyższym przykładzie są uważane za w porządku, ponieważ skaczą tylko na niewielką odległość, mają wyraźne etykiety i uwalniają kod z innych bałaganów do śledzenia warunków błędów. To pytanie zostało również omówione tutaj na temat przepełnienia stosu .
Jednak to, czego brakuje w ostatnim przykładzie, to dobry sposób na zwrócenie kodu błędu. Myślałem o dodaniu
result_code++
po każdymfree_resource_x()
wywołaniu i zwróceniu tego kodu, ale to zniwelowało niektóre przyrosty szybkości wynikające z powyższego stylu kodowania. I trudno jest zwrócić 0 w przypadku sukcesu. Może po prostu nie mam wyobraźni ;-)Więc tak, myślę, że jest duża różnica w kwestii kodowania przedwczesnych zwrotów, czy nie. Ale myślę również, że jest to widoczne tylko w bardziej skomplikowanym kodzie, którego restrukturyzacja i optymalizacja pod kątem kompilatora jest trudniejsza lub niemożliwa. Co zwykle ma miejsce, gdy w grę wchodzi alokacja zasobów.
źródło
catch
zawierająceswitch
bezbłędne oświadczenie o kodzie błędu?Chociaż nie jest to zbyt wiele odpowiedzi, kompilator produkcyjny będzie znacznie lepszy w optymalizacji niż ty. Wolałbym czytelność i łatwość konserwacji od tego rodzaju optymalizacji.
źródło
Mówiąc konkretnie,
return
zostanie skompilowany do gałęzi do końca metody, gdzie będzieRET
instrukcja lub cokolwiek to może być. Jeśli go pominiesz, koniec bloku przedelse
zostanie wkompilowany w gałąź do końcaelse
bloku. Jak więc widać, w tym konkretnym przypadku nie ma to żadnego znaczenia.źródło
Jeśli naprawdę chcesz wiedzieć, czy istnieje różnica w skompilowanym kodzie dla twojego konkretnego kompilatora i systemu, będziesz musiał sam skompilować i przyjrzeć się asemblacji.
Jednak w ogólnym rozrachunku jest prawie pewne, że kompilator może zoptymalizować lepiej niż twoje precyzyjne dostrojenie, a nawet jeśli nie jest to możliwe, jest bardzo mało prawdopodobne, aby faktycznie miało to znaczenie dla wydajności twojego programu.
Zamiast tego napisz kod w sposób najbardziej przejrzysty dla ludzi do czytania i utrzymywania, i pozwól kompilatorowi zrobić to, co robi najlepiej: wygeneruj najlepszy asembler, jaki może, ze swojego źródła.
źródło
W twoim przykładzie zwrot jest zauważalny. Co dzieje się z osobą debugującą, gdy zwracana jest strona lub dwie powyżej / poniżej, gdzie // występują różne rzeczy? Znacznie trudniej jest znaleźć / zobaczyć, gdy jest więcej kodu.
źródło
Zdecydowanie zgadzam się z blueshift: czytelność i łatwość utrzymania przede wszystkim !. Ale jeśli naprawdę się martwisz (lub po prostu chcesz dowiedzieć się, co robi Twój kompilator, co na dłuższą metę jest zdecydowanie dobrym pomysłem), powinieneś poszukać siebie.
Będzie to oznaczać używanie dekompilatora lub patrzenie na wyjście kompilatora niskiego poziomu (np. Język asemblera). W języku C # lub dowolnym języku .Net udokumentowane tutaj narzędzia zapewnią to, czego potrzebujesz.
Ale jak sam zauważyłeś, jest to prawdopodobnie przedwczesna optymalizacja.
źródło
From Clean Code: A Handbook of Agile Software Craftsmanship
w kodzie sprawi, że czytelnik przejdzie do funkcji i straci czas na czytanie foo (flaga boolowska)
Lepiej ustrukturyzowany kod daje lepsze możliwości optymalizacji kodu.
źródło
Jedna szkoła myślenia (nie pamięta jajogłowego, który to zaproponował) jest taka, że wszystkie funkcje powinny mieć tylko jeden punkt powrotu ze strukturalnego punktu widzenia, aby kod był łatwiejszy do odczytania i debugowania. To, jak sądzę, bardziej służy programowaniu debat religijnych.
Jednym z technicznych powodów, dla których możesz chcieć kontrolować, kiedy i jak kończy się funkcja, która łamie tę regułę, jest to, że kodujesz aplikacje czasu rzeczywistego i chcesz mieć pewność, że wszystkie ścieżki sterowania przez funkcję zajmują taką samą liczbę cykli zegara.
źródło
Cieszę się, że zadałeś to pytanie. Zawsze powinieneś używać gałęzi przed wcześniejszym powrotem. Dlaczego na tym poprzestać? Połącz wszystkie swoje funkcje w jedną, jeśli możesz (przynajmniej tak bardzo, jak możesz). Jest to wykonalne, jeśli nie ma rekursji. W końcu będziesz miał jedną ogromną główną funkcję, ale to jest to, czego potrzebujesz / chcesz do tego rodzaju rzeczy. Następnie zmień nazwy swoich identyfikatorów, aby były jak najkrótsze. W ten sposób, gdy kod jest wykonywany, mniej czasu spędza się na czytaniu nazw. Następnie zrób ...
źródło