Dość często można się spotkać ze (zrozumiałym) przeświadczeniem, że w C++ słowo kluczowe inline sprawi, że funkcja nim oznaczona zostanie zinline’owana1. Nic bardziej mylnego! W C++ efektywne znaczenie tego słowa jest zupełnie inne, a C++17 jeszcze trochę je zmienia.
Czym w ogóle jest inline’owanie? (ogólnie, nie w C++)
Wpis na Wikipedii. W skrócie: jest to technika optymalizacyjna, polegająca na przeniesieniu kodu wywoływanej funkcji bezpośrednio w miejsce jej wywołania. Dzięki temu, kosztem trochę większego kodu, jest on umieszczony w jednym miejscu, co często2 jest przyjazne dla cache instrukcji procesora.
Nie jest to technika bez wad, ponieważ zwiększony rozmiar plików wykonywalnych negatywnie wpływa na wydajność aplikacji. Dlatego też kompilatory korzystają z zaawansowanej heurystyki, aby zdecydować kiedy pozostawić instrukcję wywołania, a kiedy wstawić jej kod w miejsce wywołania. W niektórych przypadkach sensowne jest też wymuszenie (lub zablokowanie) stosowania tej techniki przez programistę, ale powinno być to poparte benchmarkami.
Czym nie jest inline?
Użycie inline nie oznacza wymuszenia inline’owania funkcji. Po prostu. Owszem, standard C++ mówi, że jest to wskazówka dla kompilatora:
N4140 §7.1.2 [dcl.fct.spec] / 2
The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism. An implementation is not required to perform this inline substitution at the point of call;
Ale jak widać mówi też, że implementacja może taką wskazówkę zignorować. Co więcej, brak takiej wskazówki również nie oznacza, że inline’owanie się nie odbędzie. Kompilator może zdecydować, że warto to zrobić. Pozwala na to chociażby reguła “tak jakby” (the as-if rule).
Przykład zignorowania wskazówki
Zarówno GCC 6.2 jak i Clang 4.0.0 na platformie linux x86_64 kompilowane z -O2 nie inline’ują funkcji inline_bar:
#define CALL(x) foo<x>() #define A(x) CALL((x)+1); CALL((x)+2); CALL((x)+3); CALL((x)+4); #define B(x) A(4*(x)) A(4*(x)+4) A(4*(x)+8) A(4*(x)+12) #define C(x) B(4*(x)) B(4*(x)+4) B(4*(x)+8) B(4*(x)+12) #define D(x) C(4*(x)) C(4*(x)+4) C(4*(x)+8) C(4*(x)+12) #define E D(0) D(4) D(8) D(12) template<int> void foo(); // E to kolejne wywołanie foo<1> do foo<1024>. // każda konkretyzacja foo to osobna funkcja inline void inline_bar() { E } static void bar() { E } void use_bar() { foo<42>(); bar(); foo<0>(); bar(); } void use_inline_bar() { foo<0>(); inline_bar(); foo<42>(); inline_bar(); } |
[link]
Przypadek odwrotny: funkcja zinline’owana bez podpowiedzi
max nie wywołuje funkcji max_impl, mimo że ta nie jest zadeklarowana inline:
int max_impl(int l, int r) { return l > r ? l : r; } int max(int l, int r) { return max_impl(l, r); } |
[link]
Dlatego też GCC/clang oferują __attribute__((always_inline)), a MSVC __forceinline. W przypadku MSVC jest to jednak tylko silniejszą sugestią, bez gwarancji.
To co oznacza inline?
Zezwala na wielokrotną definicję funkcji w programie. Jest to rozwiązanie problemu implementacyjnego. Zgodnie z regułą jednej definicji (One Definition Rule), każda (nie-inline) funkcja musi mieć tylko jedną definicję w programie. Za standardem:
N4140 §3.2 [basic.def.odr] / 4
Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required.
Dość często złamanie tej zasady objawia się komunikatami linkera o wielokrotnej definicji funkcji. Może tak być n.p. gdy funkcja jest zdefiniowana w nagłówku, a nagłówek include’uje więcej niż jeden plik .cpp:
// foo.h #ifndef FOO_H #define FOO_H void foo(){} #endif |
// bar.cpp #include "foo.h" |
// baz.cpp #include "foo.h" int main(){} |
Dużo częściej może się to przydarzyć przy implementacji klas:
struct foo { int bar(); }; int foo::bar() { return 42; } |
Dodanie inline do deklaracji funkcji pozwoli na takie jej użycie. Standard wymaga wtedy, aby wszystkie jej definicje były identyczne oraz aby była widoczna w każdej jednostce translacji3, w której jest użyta4. Jeśli definicje będą różne to zachowanie będzie niezdefiniowane (UB). A bardziej pragmatycznie: prawdopodobnie linker użyje pierwszej lub ostatniej napotkanej definicji, ponieważ wszystkie są (z założenia) identyczne.
C++17
W C++17 wprowadzone zostaną zmienne inline (inline variables). Pozwalają one na wielokrotną definicję zmiennej (tak jak funkcji wyżej) oznaczonej inline. Za cppreference:
Because the meaning of the keyword inline for functions came to mean “multiple definitions are permitted” rather than “inlining is preferred”, that meaning was extended to variables. (C++17)
Można je potraktować jako cukier składniowy dla następującego zapisu:
template<typename> struct foo_ { static int x; }; template<typename T> foo_<T>::x = 42; using foo = foo_<void>; // foo::x, nawet zdefiniowane w nagłówku, jest wszędzie tą samą zmienną |
Od C++17 będzie wystarczyło:
namespace foo { inline int x = 42; } // foo::x, nawet zdefiniowane w nagłówku, jest wszędzie tą samą zmienną |
Przy czym można je jak najbardziej definiować w globalnej przestrzeni nazw.
Ciekawostki
Klasy
Funkcje zdefiniowane w definicji klasy są automatycznie zadeklarowane inline:
struct foo { void bar(){} }; |
Pomimo definicji foo::bar zawartej w definicji klasy, może ona być include’owana w różnych plikach .cpp, bez żadnych problemów. Jawna deklaracja inline jest tutaj zupełnie zbędna.
Szablony
ODR ma specjalny wyjątek dla szablonów:
If D is a template and is defined in more than one translation unit, then the preceding requirements shall apply both to names from the template’s enclosing scope used in the template definition ([temp.nondep]), and also to dependent names at the point of instantiation ([temp.dep]). If the definitions of D satisfy all these requirements, then the behavior is as if there were a single definition of D.
C
W C inline ma trochę inną semantykę (uwaga: C znam średnio, możliwe, że coś przeinaczam):
- funkcja inline nie musi być widoczna w każdej jednostce translacji3, w której jest użyta jeśli funkcja jest również extern. W takim wypadku linker użyje alternatywnej definicji,
- definicje funkcji zadeklarowanej inline nie muszą być identyczne, ale działanie programu nie może zależeć od tego, która została wywołana
- zmienne statyczne wewnątrz funkcji zadeklarowanych inline w różnych jednostkach translacji3 są różnymi obiektami
1Przepraszam za Ponglish, ale nie znam sensownego tłumaczenia/polskiego zwrotu.
2To zależy od wielu czynników, kompilatory z reguły maja tutaj bardzo dobrą heurystykę.
3W uproszczeniu plik .cpp (lub .c)
4Według definicji jest to odr-used, ale to prawdopodobnie materiał na osobnego posta.
Czy przypadkiem w foo.h nie powinno nie być strażnika nagłówka? Z nim linker nie będzie się czepiał o wielokrotną definicję funkcji, a przykład miał chyba pokazać kiedy problem wystąpi 🙂
Ale w foo.h jest guard (ifndef/define/endif). To to samo, co #pragma once, ale w pełni zgodne ze standardem. Zauważ, że jest w nim pełna definicja funkcji, bez słowa kluczowego inline. Jeśli zainkludujesz ten plik z dwóch różnych plików .cpp, podczas kompilacji dostaniesz błąd linkera.
Faktycznie! Głupiutki uznałem, że jakimś magicznym sposobem linker miałby wiedzieć o guardzie i niejako tylko raz dołączyłby definicję funkcji. Tak to jest jak na sucho nocą przegląda się kod 😛 Wszystko się zgadza, dzięki!
Dzieki za przydatny artykul.
Jedna uwaga:
“Dzięki temu, kosztem trochę większego kodu, jest on umieszczony w jednym miejscu, co jest przyjazne dla cache instrukcji procesora.”
Moim zdaniem inline’owanie moze byc korzystne z punktu widzenia cache’a instrukcji (wieksza lokalnosc) ale moze tez byc bardzo niekorzystne. Przyklad:
Gdy func_a miesci sie w cache’u instrukcji ale func_a ze zinline’owana func_b juz sie nie miesci to wydajnosc:
bedzie drastycznie gorszy jesli func_b zostanie zinlinowana.
Zmieniłem trochę opis, czy teraz lepiej?
Mysle, ze obecna wersja jest duzo bezpieczniejsza – dosc ciezko jest generalizowac w temacie optymalizacji :).