W jakim stopniu wywołania funkcji wpływają na wydajność?

13

Wydobywanie funkcjonalności na metody lub funkcje jest niezbędne dla modułowości kodu, czytelności i interoperacyjności, szczególnie w OOP.

Ale to oznacza, że ​​będzie więcej wywołań funkcji.

W jaki sposób podział naszego kodu na metody lub funkcje faktycznie wpływa na wydajność w nowoczesnych * językach?

* Najpopularniejsze: C, Java, C ++, C #, Python, JavaScript, Ruby ...

dabadaba
źródło
1
Wydaje mi się, że każda implementacja językowa warta swojej uwagi ma już kilkadziesiąt lat. IOW, narzut wynosi dokładnie 0
Jörg W Mittag
1
„więcej wywołań funkcji zostanie wykonanych” często nie jest prawdą, ponieważ wiele z tych wywołań zostanie zoptymalizowanych przez różne kompilatory / interpretatory przetwarzające Twój kod i wstawiające różne rzeczy. Jeśli twój język nie ma tego rodzaju optymalizacji, może nie uważam go za nowoczesny.
Ixrec
2
Jak wpłynie to na wydajność? Spowoduje to, że będzie on szybszy lub wolniejszy, albo go nie zmieni, w zależności od tego, jakiego języka używasz i jaka jest struktura faktycznego kodu oraz być może jakiej wersji kompilatora używasz, a może nawet jakiej platformy używasz ” ponownie działa. Każda otrzymana odpowiedź będzie odmianą tej niepewności, z większą ilością słów i dowodów potwierdzających.
GrandOpener
1
Wpływ, jeśli w ogóle, jest tak mały, że Ty, osoba, nigdy go nie zauważysz. Są o wiele ważniejsze rzeczy, o które należy się martwić. Podobnie jak to, czy tabulatory powinny mieć 5 czy 7 spacji.
MetaFight,

Odpowiedzi:

21

Może. Kompilator może zdecydować: „hej, ta funkcja jest wywoływana tylko kilka razy i mam zoptymalizować szybkość, więc po prostu wstawię tę funkcję”. Zasadniczo kompilator zastąpi wywołanie funkcji treścią funkcji. Na przykład kod źródłowy wyglądałby tak.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Kompilator decyduje się na wstawienie DoSomethingElsei kod staje się

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Gdy funkcje nie są wstawiane, tak, istnieje wywołanie wydajności, aby wykonać wywołanie funkcji. Jest to jednak tak niewielki hit, że tylko bardzo wysokowydajny kod będzie martwił się wywołaniami funkcji. W tego typu projektach kod jest zwykle zapisywany w asemblerze.

Wywołania funkcji (w zależności od platformy) zwykle obejmują kilka dziesiątych instrukcji, w tym zapisywanie / przywracanie stosu. Niektóre wywołania funkcji składają się z instrukcji skoku i powrotu.

Ale są też inne rzeczy, które mogą wpłynąć na wydajność wywołania funkcji. Wywoływana funkcja może nie zostać załadowana do pamięci podręcznej procesora, powodując brak pamięci podręcznej i zmuszając kontroler pamięci do pobrania funkcji z głównej pamięci RAM. Może to spowodować duży hit wydajności.

W skrócie: wywołania funkcji mogą, ale nie muszą, wpływać na wydajność. Jedynym sposobem, aby to powiedzieć, jest profilowanie kodu. Nie próbuj zgadywać, gdzie są wolne miejsca kodu, ponieważ kompilator i sprzęt mają niesamowite sztuczki. Profiluj kod, aby uzyskać lokalizację wolnych miejsc.

CHendrix
źródło
1
Widziałem z nowoczesnymi kompilatorami (gcc, clang) w sytuacjach, w których naprawdę zależało mi na tym, że stworzyły całkiem zły kod dla pętli wewnątrz dużej funkcji . Wyodrębnienie pętli do funkcji statycznej nie pomogło z powodu wstawiania. Wyodrębnienie pętli do funkcji zewnętrznej stworzyło w niektórych przypadkach znaczne (mierzalne w testach porównawczych) ulepszenia prędkości.
gnasher729
1
Chciałbym przyłączyć się do tego i powiedzieć, że OP powinien uważać na optymalizację przedwczesną
Patrick
1
@Patrick Bingo. Jeśli zamierzasz zoptymalizować, użyj profilera, aby zobaczyć, gdzie są wolne sekcje. Nie zgaduj. Zazwyczaj można sprawdzić, gdzie mogą znajdować się wolne sekcje, ale potwierdź to za pomocą profilera.
CHendrix,
@ gnasher729 Aby rozwiązać ten konkretny problem, potrzeba czegoś więcej niż profilera - trzeba nauczyć się czytać również zdemontowany kod maszynowy. Chociaż istnieje przedwczesna optymalizacja, nie ma czegoś takiego jak przedwczesne uczenie się (przynajmniej w rozwoju oprogramowania).
rwong
Państwo może mieć ten problem, jeśli wywołanie funkcji milion razy, ale są bardziej narażone na inne problemy, które mają znacznie większy wpływ.
Michael Shaw
5

Jest to kwestia implementacji kompilatora lub środowiska wykonawczego (i jego opcji) i nie można tego powiedzieć z całą pewnością.

W kompilatorach C i C ++ niektóre kompilatory będą wprowadzać wywołania w oparciu o ustawienia optymalizacji - można to w prosty sposób sprawdzić, badając wygenerowany zestaw, patrząc na narzędzia takie jak https://gcc.godbolt.org/

Inne języki, takie jak Java, mają to jako część środowiska wykonawczego. Jest to część JIT i rozwinięta w tym pytaniu SO . W szczegółowym spojrzeniu na opcje JVM dla HotSpot

-XX:InlineSmallCode=n Wstaw wcześniej skompilowaną metodę tylko wtedy, gdy jej wygenerowany rodzimy rozmiar kodu jest mniejszy niż ten. Wartość domyślna różni się w zależności od platformy, na której działa JVM.
-XX:MaxInlineSize=35 Maksymalny rozmiar kodu bajtowego metody do wstawienia.
-XX:FreqInlineSize=n Maksymalny rozmiar kodu bajtowego często wykonywanej metody do wstawienia. Wartość domyślna różni się w zależności od platformy, na której działa JVM.

Tak więc, kompilator HotSpot JIT wprowadzi metody spełniające określone kryteria.

Wpływ tego, trudno jest określić, jak każdy JVM (lub kompilator) może robić rzeczy inaczej i próbuje odpowiedzieć szerokim pociągnięciem języka jest prawie pewność źle. Wpływ można określić tylko poprzez profilowanie kodu w odpowiednim środowisku uruchomieniowym i sprawdzenie skompilowanych danych wyjściowych.

Można to postrzegać jako błędne podejście, gdy CPython nie jest wbudowany, ale w Jython (Python działający w JVM) z niektórymi wywołaniami. Podobnie MRI Ruby nie wstawia się, gdy robi to JRuby, i ruby2c, który jest transpilatorem ruby ​​do C ... który może być wstawiany lub nie w zależności od opcji kompilatora C, z którymi został skompilowany.

Języki nie są wbudowane. Realizacje mogą .

użytkownik227864
źródło
5

Szukasz wydajności w niewłaściwym miejscu. Problem z wywołaniami funkcji nie polega na tym, że kosztują dużo. Jest inny problem. Wywołania funkcji mogą być całkowicie darmowe, a ty nadal będziesz mieć ten inny problem.

Jest tak, że funkcja jest jak karta kredytowa. Ponieważ możesz go łatwo używać, zwykle używasz go częściej niż powinieneś. Załóżmy, że nazywasz to 20% więcej, niż potrzebujesz. Następnie typowe duże oprogramowanie zawiera kilka warstw, z których każde wywołuje funkcje w warstwie poniżej, więc współczynnik 1,2 może zostać złożony z liczbą warstw. (Na przykład, jeśli jest pięć warstw, a każda warstwa ma współczynnik spowolnienia równy 1,2, złożony współczynnik spowolnienia wynosi 1,2 ^ 5 lub 2,5.) To tylko jeden sposób, aby o tym pomyśleć.

Nie oznacza to, że należy unikać wywołań funkcji. Oznacza to, że kiedy kod jest uruchomiony, powinieneś wiedzieć, jak znaleźć i wyeliminować marnotrawstwo. Istnieje wiele doskonałych porad, jak to zrobić w witrynach Stackexchange. To jeden z moich wkładów.

DODANO: Mały przykład. Kiedyś pracowałem w zespole oprogramowania na poziomie fabryki, który śledził szereg zleceń pracy lub „zleceń”. Była funkcja, JobDone(idJob)która mogła stwierdzić, czy zadanie zostało wykonane. Zadanie zostało wykonane, gdy wszystkie jego poddziałania zostały wykonane, a każde z nich zostało wykonane, gdy wszystkie jego podoperacje zostały wykonane. Wszystkie te rzeczy były śledzone w relacyjnej bazie danych. Pojedyncze wywołanie innej funkcji może wyodrębnić wszystkie te informacje, tzw JobDone. Tę inną funkcję, sprawdzić, czy zadanie zostało wykonane, i odrzucić resztę. Wtedy ludzie mogliby łatwo napisać taki kod:

while(!JobDone(idJob)){
    ...
}

lub

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Widzisz sens? Ta funkcja była tak „potężna” i łatwa do wywołania, że ​​nazbyt się nazywała. Problemem wydajności nie były więc instrukcje wchodzące i wychodzące z funkcji. Chodziło o to, aby istniał bardziej bezpośredni sposób na stwierdzenie, czy zadania zostały wykonane. Ponownie ten kod mógł zostać osadzony w tysiącach wierszy kodu, który byłby niewinny. Wszyscy starają się to naprawić, ale to tak, jakby rzucać lotkami w ciemnym pokoju. Zamiast tego potrzebujesz go uruchomić, a następnie pozwolić, aby „powolny kod” powiedział ci, co to jest, po prostu przez poświęcenie czasu. Do tego używam losowego wstrzymywania .

Mike Dunlavey
źródło
1

Myślę, że to naprawdę zależy od języka i funkcji. Podczas gdy kompilatory c i c ++ mogą wstawiać wiele funkcji, nie dotyczy to Pythona ani Javy.

Chociaż nie znam konkretnych szczegółów dla java (z wyjątkiem tego, że każda metoda jest wirtualna, ale sugeruję, abyś lepiej sprawdził dokumentację), w Pythonie jestem pewien, że nie ma wbudowania, optymalizacji optymalizacji ogona, a wywołania funkcji są dość drogie.

Funkcje Pythona są w zasadzie obiektami wykonywalnymi (i tak naprawdę można również zdefiniować metodę call (), aby uczynić instancję obiektu funkcją). Oznacza to, że dzwonienie do nich wiąże się z dużym nakładem pracy ...

ALE

kiedy definiujesz zmienne wewnątrz funkcji, interpreter używa LOADFAST zamiast normalnej instrukcji LOAD w kodzie bajtowym, dzięki czemu twój kod jest szybszy ...

Inną rzeczą jest to, że podczas definiowania obiektu, który można wywołać, możliwe są wzorce takie jak zapamiętywanie i mogą one znacznie przyspieszyć obliczenia (kosztem zużycia większej ilości pamięci). Zasadniczo zawsze jest to kompromis. Koszt wywołań funkcji zależy również od parametrów, ponieważ określają one, ile rzeczy trzeba skopiować na stosie (dlatego w c / c ++ powszechną praktyką jest przekazywanie dużych parametrów, takich jak struktury, za pomocą wskaźników / referencji zamiast według wartości).

Myślę, że twoje pytanie jest w praktyce zbyt ogólne, aby można było na nie odpowiedzieć całkowicie przy wymianie stosów.

Sugeruję, abyś zaczął od jednego języka i przestudiował zaawansowaną dokumentację, aby zrozumieć, w jaki sposób wywołania funkcji są implementowane przez ten konkretny język.

Będziesz zaskoczony, jak wiele rzeczy nauczysz się w tym procesie.

Jeśli masz konkretny problem, dokonuj pomiarów / profilowania i decyduj o pogodzie, lepiej jest utworzyć funkcję lub skopiować / wkleić równoważny kod.

myślę, że jeśli zadasz bardziej szczegółowe pytanie, łatwiej byłoby uzyskać bardziej szczegółową odpowiedź.

ingframin
źródło
Cytując cię: „Myślę, że twoje pytanie jest w praktyce zbyt ogólne, aby można było na nie odpowiedzieć całkowicie przy wymianie stosów”. Jak mogę to zawęzić? Bardzo chciałbym zobaczyć niektóre rzeczywiste dane reprezentujące wpływ wywołania funkcji na wydajność. Nie obchodzi mnie, w jakim języku, ciekawi mnie tylko bardziej szczegółowe wyjaśnienie, w miarę możliwości poparte danymi, tak jak powiedziałem.
dabadaba
Chodzi o to, że zależy to od języka. W C i C ++, jeśli funkcja jest wstawiona, wpływ wynosi 0. Jeśli nie jest wstawiony, zależy to od jego parametrów, czy znajduje się w pamięci podręcznej, czy nie, itp.
ingframin
1

Jakiś czas temu zmierzyłem narzut bezpośrednich i wirtualnych wywołań funkcji C ++ na Xenon PowerPC .

Funkcje, o których mowa, miały jeden parametr i jeden powrót, więc przekazywanie parametrów miało miejsce w rejestrach.

Krótko mówiąc, narzut związany z bezpośrednim (nie wirtualnym) wywołaniem funkcji wyniósł około 5,5 nanosekundy lub 18 cykli zegara, w porównaniu z wywołaniem funkcji wbudowanej. Narzut wywołania funkcji wirtualnej wyniósł 13,2 nanosekundy lub 42 cykli zegarowych, w porównaniu do wbudowanego.

Te czasy są prawdopodobnie różne dla różnych rodzin procesorów. Mój kod testowy jest tutaj ; możesz uruchomić ten sam eksperyment na swoim sprzęcie. Użyj precyzyjnego timera, takiego jak rdtsc, do implementacji CFastTimer; system time () nie jest wystarczająco dokładny.

Crashworks
źródło