[9] Funkcje inline
(Część C++ FAQ Lite, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)


FAQ - sekcja [9]:


[9.1] O co wogóle chodzi z tymi funkcjami inline? UPDATED!

[Recently odpowiedź napisana na nowo, dodano kilka nowych punktów (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Kiedy kompilator natrafia na wywołanie funkcji inline, wstawia w to miejsce kod tej funkcji [w normalnym przypadku kompilator wstawia kod przekazujący parametry funkcji i wywołujący ją - przyp. tłum.] - koncepcyjnie przypomina to rozwijanie makra #define. Pozwala to, w zależności od "zilionów" innych czynników, na zwiększenie wydajności programu, ponieważ moduł optymalizujący zaszyty w kompilatorze może proceduralnie zintegrować kod wywoływanej funkcji z kodem funkcji ją wywołującej.

Na ogół funkcję deklaruje się jako inline przy użyciu słowa kluczowego inline, ale można to zrobić bez niego. Jednakże w jakikolwiek sposób zadeklarujesz, że funkcja ma być inline, deklaracja ta jest jedynie "propozycją", którą kompilator może zignorować: kompilator rozwinie niektóre, wszystkie lub żadne z wywołań funkcji inline. (Nie rozczarowywuj się tą beznadziejną niejasnością. Elastyczność przy rozwijaniu wywołań funkcji inline, wbrew pozorom, jest potężną zaletą: pozwala ona kompilatorowi różnie traktować duże i małe funkcje, a dodatkowo zawsze możesz kazać kompilatorowi wygenerować łatwy do zdebugowania kod wynikowy, jeśli wyłączysz w nim rozwijanie funkcji inline).

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.2] Interesuje mnie prosty przykład integracji proceduralnej. NEW!

[Recently created (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Rozpatrzmy poniższy kod, w którym następuje wywołanie funkcji g():

 void f()
 {
   int x = 
/*...*/;
   int y = 
/*...*/;
   int z = 
/*...*/;
   
// ...kod wykorzystujący zmienne xy i z...
   g(x, y, z);
   
// ...jeszcze więcej kodu wykorzystującego xy i z...
 }

W typowej implementacji wywołania funkcji w języku C++, wykorzystującej rejestry procesora i stos, tuż przed wywołaniem funkcji g() stan rejestrów oraz parametry są zapisywane na stos; następnie funkcja g() odczytuje parametry ze stosu, i tuż przed zakończeniem swojego działania przywraca wcześniejszy stan rejestrów; później sterowanie wraca do funkcji f(). Przebieg wywołania funkcji wymaga więc kilkukrotnego zapisywania i odczytywania tych samych danych, zwłaszcza gdy kompilator umieści zmienne x, y i z w rejestrach: każda zmienna jest wtedy dwukrotnie zapisywana (jako parametr i do rejestru) oraz dwukrotnie odczytywana (gdy zostaje użyta w funkcji g() i podczas przywracania stanu rejestrów w czasie powrotu do funkcji f()).

 void g(int x, int y, int z)
 {
   
// ...kod wykorzystujący zmienne xy i z...
 }

Jeśli kompilator wstawi kod funkcji g() w miejsce jej wywołania, wszystkie te operacje na pamięci nagle znikną. Nie nastąpi zapis-do ani odczyt-z rejestrów ponieważ nie będzie wywołania funkcji, a parametry nie zostaną przesłane do/z stosu ponieważ kompilator będzie wiedział, że one są już w rejestrach procesora.

Oczywiście istnieją "ziliony" czynników, których tu nie uwzględniłem, ale powyższe może służyć za przykład jednego z wielu kroków, jakie kompilator może podjąć wtrakcie integracji proceduralnej.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.3] Czy funkcje inline zwiększają wydajność? UPDATED!

[Recently rewrote, added numerous new points (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Tak i nie. Czasem. Może.

Na to pytanie nie ma prostej odpowiedzi. Funkcje inline mogą zarówno przyśpieszyć jak i spowolnić wykonywanie kodu. Mogą też zwiększyć rozmiar kodu wynikowego lub go zmniejszyć. Wreszcie mogą być, i najczęściej są, zupełnie niezwiązane z szybkością programu.

Funkcje inline mogą przyśpieszyć program: Jak pokazano w poprzednim faqu, integracja proceduralna może wyeliminować całą masę niepotrzebnych instrukcji, które do tej pory spowalniały program.

Funkcje inline mogą spowolnić program: Zbyt duża liczba rozwinięć funkcji inline może spowodować rozrost kodu (ang. "code bloat" - spuchnięcie kodu) - kod wtedy może zająć więcej stron w pamięci, co może doprowadzić do ich "przerzucania" (ang. thrashing) przez system operacyjny obsługujący wirtualną pamięć. Inaczej mówiąc, jeżeli kod wynikowy programu będzie zbyt duży, system będzie tracił masę czasu na przesyłanie fragmentów kodu między pamięcią fizyczną a dyskiem.

Funkcje inline mogą powiększyć program: Wynika to ze wspomnianego wcześniej rozrostu kodu. Np. jeśli program zawiera 100 funkcji inline, każda z nich po skompilowaniu zajmuje 100 bajtów i jest wywoływana w 100 miejscach w programie, to spowoduje to zwiększenie rozmiaru kodu wynikowego o 1MB. Czy ten 1MB stanowi jakiś problem? Kto wie, ale zawsze jest możliwe, że ten ostatni megabajt kodu nie zmieści się w pamięci i zmusi system operacyjny do korzystania z dysku (wspomniany wcześniej "thrashing"), co spowoduje pogorszenie wydajności.

Funkcje inline mogą zmniejszyć program: W przypadku bardzo krótkich funkcji kompilator często generuje więcej kodu maszynowego mającego odczytywać/zapisywać parametry i rejestry na stos, niż mającego wykonać instrukcje zawarte w tej funkcji. Zdarza się to również przy dużych funkcjach, kiedy optymalizator może usunąć sporą ilość zbędnego kodu poprzez integrację proceduralną i tym samym zmniejszyć tę funkcję do niewielkich rozmiarów.

Funkcje inline mogą przyczynić się do częstego "przerzucania" stron między pamięcią a twardym dyskiem: Stosowanie funkcji inline może zwiększyć rozmiar kodu wynikowego i w efekcie doprowadzić do "przerzucania" stron [większy rozmiar kodu zwiększa prawdopodobieństwo, że nie zmieści się on w pamięci fizycznej i jego fragmenty będą często przesyłane między dyskiem a RAMem - przyp. tłum.]

Funkcje inline mogą zapobiec "przerzucaniu" stron między pamięcią a dyskiem: Po zastosowaniu funkcji inline zbiór roboczy programu (ang. "working set" - liczba stron, która musi się w danej chwili znajdować w RAMie) może ulec zmniejszeniu nawet, jeżeli wzrósł rozmiar kodu wynikowego. Jako przykład rozpatrzmy dwie funkcje: f() i g(); w pewnym miejscu pierwsza z nich wywołuje drugą. W "normalnym" przypadku (obie funkcje nie są inline), kompilator może rozmieścić je na osobnych stronach [w takim przypadku zawsze istnieje jakieś prawdopodobieństwo, że jedna z tych stron będzie się znajdować na dysku - wtedy wywołanie funkcji g() lub powrót do funkcji f() wymagać będzie przesłania tej strony do RAMu - przyp. tłum.]. Natomiast jeżeli zdefiniujemy te funkcje jako inline, kompilator będzie mógł zintegrować proceduralnie wywołanie funkcji g() wewnątrz funkcji f() - w takim przypadku najczęściej obie funkcje znajdą się na jednej stronie [i nie będzie konieczne pobieranie "tej drugiej" z dysku - przyp. tłum.].

Funkcje inline mogą w ogóle nie wpłynąć na szybkość : W większości systemów komputerowych głównym czynnikiem ograniczającym wydajność nie jest wcale szybkość działania procesora, ale głównie wolna komunikacja z urządzeniami, bazami danych, siecią. Z wyjątkiem sytuacji, gdy procesor jest obciążony w 100%, "przesiadka" na funkcje inline prawdopodobnie nie przyśpieszy działania programu. (Nawet w systemach, w których właśnie szybkość procesora jest głównym czynnikiem determinującym wydajność, zastosowanie funkcji inline pomaga jedynie w "wąskich gardłach" programu, które na ogół występują w nieznacznej części kodu.)

Nie ma prostych odpowiedzi: Musisz sam trochę poeksperymentować, żeby stwierdzić co jest najlepsze w danym przypadku. Nie opieraj się na uproszczonych sądach w stylu "Nigdy nie używaj funkcji inline" lub "Zawsze używaj funkcji inline" lub "Używaj funkcji inline wtedy i tylko wtedy, gdy kod funkcji zajmuje mniej niż N linijek". Tego rodzaju zasady-na-każdą-okazję są może i łatwe do zapisania, ale ich rezultaty są najwyżej suboptymalne.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.4] W jaki sposób funkcje inline pomagają uzyskać kompromis między bezpieczeństwem a szybkością działania? UPDATED!

[Recently Wersja polska - poprawiono drugi akapit. (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

W języku C, strukturę z enkapsulacją można było uzyskać przez umieszczenie w niej wskaźnika void* wskazującego do danych, które miały pozostać ukrytymi dla użytkowników struktury. Programista mógł nie wiedzieć w jaki sposób interpretować dane wskazywane przez void*a, ale funkcje operujące na strukturze rzutowały void*a na wskaźnik odpowiedniego typu (którego definicja była ukryta gdzieś w kodzie programu). Dawało to pewien rodzaj enkapsulacji.

Niestety, stosując tę metodę traciło się zalety kontroli typu. Ponadto wymuszała ona wywoływanie funkcji aby uzyskać dostęp nawet do najbardziej trywialnych składowych struktury (jeśli zezwolonoby na bezpośredni dostęp do wszystkich jej składowych, użytkownik z dowolnego miejsca w programie w dowolnym momencie mógłby mieć dostęp do nich, gdyż z konieczności musiałby wiedzieć jak interpretować dane wskazywane przez "void*a"; to z kolei utrudniałoby wprowadzanie późniejszych zmian do struktury).

Wywołanie funkcji może stanowić niewielki koszt, jednak zawsze jakiś. Funkcje zaszyte wewnątrz klas w C++ mogą być rozwijane w miejscu wywołania dzięki słowu kluczowemu inline. Dzięki temu możliwe jest jednoczesne osiągnięcie bezpieczeństwa związanego z enkapsulacją oraz szybkości wynikającej z bezpośredniego dostępu i braku wywołania funkcji. Warto tu też dodać, że - podobnie jak w przypadku zwykłych funkcji - tak i w funkcjach inline kompilator sprawdza, czy przekazane parametry są odpowiednich typów (co stanowi ulepszenie w stosunku do makr "define").

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.5] Dlaczego powinienem używać funkcji inline functions zamiast starych makr #define? UPDATED!

[Recently wordsmithed (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Ponieważ makra #definezłe, i to aż poczwórnie: złe#1, złe#2, złe#3, i złe#4. Czasem ich użycie jest konieczne i można zrezygnować z tej zasady, ale one i tak wciąż będą "złe".

W odróżnieniu od makr #define, funkcje inline pozwalają uniknąć starych, znanych błędów związanych z użyciem makr, ponieważ funkcje tylko raz obliczają każde wyrażenie przekazane im jako parametr. Inaczej mówiąc, wywołanie funkcji inline niczym się nie różni od wywołania zwykłej funkcji, poza tym że jest szybsze:

 // Makro obliczające wartośc bezwzględna z i.
 #define unsafe(i)  \
         ( (i) >= 0 ? (i) : -(i) )
 
 
// Funkcja inline zwracająca wartość bezwzględna z i.
 inline
 int safe(int i)
 {
   return i >= 0 ? i : -i;
 }
 
 int f();
 
 void userCode(int x)
 {
   int ans;
 
   ans = unsafe(x++);   
// Błąd! x jest zwiększany dwukrotnie
   ans = unsafe(f());   
// Niebezpieczne! f() jest dwukrotnie wywoływane
 
   ans = safe(x++);     
// Dobrze! x jest zwiększany tylko raz
   ans = safe(f());     
// Dobrze! f() jest wywoływany tylko raz
 }

Oprócz tego, parametry przekazywane funkcji inline są sprawdzane pod kątem zgodności typów, a ewentualne niejawne rzutowania są prawidłowo wykonywane.

Makra są niedobre dla Twojego zdrowia; lepiej ich nie używaj chyba, że nie ma innej możliwości.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.6] W jaki sposób mogę poinformować kompilator, że funkcja nie będąca metodą ma być inline?

Kiedy deklarujesz funkcję inline, wygląda ona tak samo jak zwykła funkcja:

 void f(int i, char c);

Jednak gdy definiujesz tę funkcję, dopisujesz przed nią słowo kluczowe inline, a całą definicję umieszczasz w pliku nagłówkowym.

 inline
 void f(int i, char c)
 {
   
// ...
 }

Uwaga: Konieczne jest, aby definicja funkcji inline znajdowała się w pliku nagłówkowym, chyba że funkcja ta jest wykorzystywana tylko w jednym pliku .cpp. W przeciwnym wypadku, jeżeli w jednym pliku .cpp będzie się znajdować definicja funkcji, a w drugim jej wywołanie to przy generowaniu kodu wynikowego programu linker (konsolidator) zgłosi błąd "nieprawidłowe wiązanie zewnętrzne" (ang. "unresolved external").

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.7] W jaki sposób mogę poinformować kompilator, że metoda ma być funkcją inline?

Deklaracja metody inline wygląda tak jak deklaracja każdej innej metody:

 class Fred {
 public:
   void f(int i, char c);
 };

Jednakże definicja metody inline różni się od definicji zwykłej metody tym, że poprzedza ją słowo kluczowe inline. Ponadto definicja metody inline powinna być umieszczona w pliku nagłówkowym.

 inline
 void Fred::f(int i, char c)
 {
   
// ...
 }

Zazwyczaj konieczne jest, aby definicja metody inline znajdowała się w pliku nagłówkowym. Jeśli byłaby ona umieszczona w jednym pliku .cpp, a w innym pliku znajdowałoby się jej wywołanie to wtrakcie generowania kodu wynikowego programu linker zgłosiłby błąd "nieprawidłowe wiązanie zewnętrzne" (ang. "unresolved external").

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[9.8] Czy można w inny sposób poinformować kompilator, że metoda ma być funkcją inline?

Owszem: można zdefiniować metodę w obrębie definicji klasy:

 class Fred {
 public:
   void f(int i, char c)
     {
       
// ...
     }
 };

Mimo że ten sposób wydaje się być prostszy dla kogoś implementującego klasę, na ogół utrudnia on zrozumienie działania klasy osobom czytającym później ten kod, z powodu wymieszania ze sobą informacji o tym "co" klasa robi i "jak" to robi. Z tego powodu metody przeważnie się definiuje poza obszarem definicji klasy używając słowa kluczowego inline. Pogląd ten nabiera sensu przy następującym spojrzeniu: wielu ludzi może korzystać z Twojej klasy, ale jest tylko jeden człowiek, który ją zaprojektował (ty, rzecz jasna); wobec tego powinieneś podejmować decyzje korzystniejsze raczej dla większości, niż dla mniejszości.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


E-Mail E-mail the author
C++ FAQ LiteSpis treściSkorowidzO autorze©Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004