2016-01-05 16 views
5

Załóżmy, że mam następujący kod:Zaproponuj do kompilatora do selektywnego funkcja inline wzywa

struct Foo { 
    void helper() { ... } 
    void fast_path() { ...; helper(); ... } 
    void slow_path1() { ...; helper(); ... } 
    void slow_path2() { ...; helper(); ... } 
}; 

Sposób fast_path() jest wydajność krytyczna i tak każdy (rozsądny) należy dołożyć starań, aby go tak szybko, jak to możliwe. Metody nie są krytyczne z punktu widzenia wydajności.

Z mojego zrozumienia, typowy kompilator może spojrzeć na ten kod i zadecydować o inline helper() jeśli jest na tyle skomplikowane, aby zmniejszyć całkowity rozmiar instrukcji, jak helper() dzielone między funkcjami wielu metod. Ten sam kompilator może wstawiać helper(), jeśli metody wolnej ścieżki nie istnieją.

biorąc pod uwagę nasze żądane właściwości użytkowe, chcemy kompilator do inline wywołanie helper() wewnątrz fast_path(), ale wolą domyślne zachowanie kompilatora w slow_path1() i slow_path2().

Jednym rozwiązaniem jest mieć definicje funkcji zwolnionym ścieżka i wezwanie do fast_path() żywo w oddzielnych jednostkach kompilacji, tak, że kompilator nie widzi wykorzystanie helper() dzieloną z fast_path(). Zachowanie tego rozdziału wymaga szczególnej ostrożności i nie może być egzekwowane przez kompilator. Ponadto rozprzestrzenianie plików (Foo.h, FooINLINES.cpp, a teraz także Foo.cpp) jest niepożądane, a dodatkowe jednostki kompilacji komplikują kompilację, która być może była biblioteką tylko nagłówkową.

Czy istnieje lepszy sposób?

Idealnie chciałbym nową do_not_inline_function_calls_inside_me C++ słowo kluczowe, które mogę używać tak:

do_not_inline_function_calls_inside_me void slow_path1() { ... } 
    do_not_inline_function_calls_inside_me void slow_path2() { ... } 

Alternatywnie inline_function_calls_inside_me słowo kluczowe, na przykład:

inline_function_calls_inside_me void fast_path() { ... } 

Zauważ, że te hipotetyczne słowa kluczowe ozdobić metody *_path*(), a nie metody.

Przykładowy kontekst, w którym można mieć takie wymagania wydajnościowe, to konkurs programistyczny, w którym każdy uczestnik pisze aplikację, która słucha rozrzuconych globalnych transmisji danych typu A i B. Po otrzymaniu transmisji typu B każda aplikacja musi wykonać obliczenia, które zależą od sekwencji wcześniej nadawanych komunikatów typu-A, i prześlij wynik obliczeń do centralnego serwera. Pierwszy poprawny respondent do każdej transmisji typu B otrzymuje punkt. Natura problemu obliczeniowego może pozwolić na wykonanie precomputation na aktualizacjach typu A; nie ma żadnej korzyści, aby robić to szybko.

+0

Techniki zapobiegające wstawianiu funkcji są specyficzne dla kompilatora. Tak samo jak techniki gwarantujące funkcję. W każdym razie twoje założenie jest błędne i pokazuje mentalność "przedwczesnej optymalizacji". Scenariusz, w którym jedna funkcja jest "krytyczna pod względem wydajności", jest tak rzadki w praktyce, że nie ma sensu. Programiści są znacznie mniej zdolni, niż sądzą, że identyfikują hotspoty wydajności w swoim kodzie - dlatego narzędzia takie jak profilery istnieją. – Peter

+0

@Peter Wydajność krytycznych ścieżek kodowych może być w praktyce rzadkością, ale one istnieją. Jestem szczególnie zainteresowany gcc. – dshin

+0

W gcc możesz podać '__attribute __ ((always_inline))'. –

Odpowiedz

3

Ogólnie mówiąc, nie powinieneś starać się być mądrzejszy od kompilatora. Współczesne kompilatory wykonują niesamowitą robotę decydując o funkcjach wbudowanych, a ludzie mają na ogół złe pojęcie o tym.

Z mojego doświadczenia, najlepiej można zrobić, to mieć wszystkie istotne funkcje tam jak inline funkcji w tej samej jednostce tłumaczeniowej więc kompilator widzi ich definicji i może inline je według własnego uznania. Levae ostateczna decyzja, czy wstawiać daną funkcję do kompilatora, i używać "wymuszonego inline" bardzo oszczędnie, chyba że masz dowody na to, że ma ona korzystny wpływ w danej sytuacji.

Aby ułatwić pracę kompilatorowi, można podać mu dodatkowe informacje o programie. W GCC i Clang możesz użyć do tego celu function attributes.

struct Foo { 
    void helper(); 
    void fast_path() __attribute__ ((hot)); 
    void slow_path1() __attribute__ ((cold)); 
    void slow_path2() __attribute__ ((cold)); 
}; 

inline void Foo::helper()  { … } 
inline void Foo::fast_path() { … } 
inline void Foo::slow_path1() { … } 
inline void Foo::slow_path2() { … } 

To będzie wskazywać kompilator zoptymalizować Foo::fast_path bardziej agresywnie na szybkość i Foo::slow_path1 i Foo::slow_path2 dla małej podręcznej obudowie. Jeśli którakolwiek z tych funkcji wywoła Foo::helper, może zdecydować, w każdym przypadku, czy ją wprowadzić, czy nie. (Aby uzyskać dokładny efekt adnotacji, zapoznaj się z dokumentacją w dołączonej instrukcji).

Jeszcze lepszym sposobem na wskazanie kompilatora jest podanie rzeczywistych danych profilowania. Za pomocą GCC możesz skompilować swój program za pomocą opcji -fprofile-generate. To przysłoni twój plik binarny za pomocą kodu, który zbiera statystyki profilu. Teraz uruchom program z reprezentatywnym zestawem wejść. Spowoduje to utworzenie pliku *.gcda z zebranymi danymi profilu. Teraz ponownie skompiluj z opcją -fprofile-use. GCC wykorzysta zebrane informacje o profilu, aby zdecydować, które ścieżki w kodzie są gorące i jak wchodzą ze sobą w interakcje. Ta technika jest znana jako optymalizacja profilowana (PGO).

Oczywiście, jeśli obawiasz się takich rzeczy, najpierw upewnij się, że włączasz odpowiednie poziomy optymalizacji (-O2). Zwłaszcza szablonowy kod C + (tj. Prawie wszystko, co używa standardowej biblioteki lub Boost) może generować naprawdę paskudny kod maszynowy po skompilowaniu bez przyzwoitej optymalizacji. Zastanów się również, czy chcesz skompilować jony assert do swojego kodu (-DNDEBUG).

+0

Gorące/zimne atrybuty wyglądają dokładnie tak, jak chciałem. Nigdy o nich nie wiedziałam - dzięki! – dshin

+0

Odnośnie do Twojej sugestii PGO, jednak: jeśli spojrzysz na moją przykładową aplikację w ostatnim akapicie mojego posta, zobaczysz, że nie jest dobrym kandydatem do PGO, jeśli aktualizacje typu A znacznie przewyższają liczbę typów B. – dshin

+1

Tak, to może być trudne. Może możesz stworzyć sekwencję wejściową dla przebiegu profilowania, który wyzwala więcej zdarzeń * B *? Ale to śliskie zbocze. Jeśli trudno jest zebrać dane profilowania, pozostawanie przy atrybutach ręcznych jest prawdopodobnie najlepszym rozwiązaniem. – 5gon12eder

Powiązane problemy