Psotna optymalizacja
Wstęp
Jak wiadomo, projekty w Visual Studio mają predefiniowane dwie opcje kompilacji – Debug i Release. Najczęściej korzysta się z tej pierwszej, ponieważ umożliwia ona swobodne „odpluskwianie” z całym dobrodziejstwem oferowanego przez VS przybornika narzędziowego. Z kolei opcji „Release” używa się do skompilowania projektów do formy gotowej do użycia w środowisku produkcyjnym. Czym to się tak naprawdę różni?
Polecam świetnie napisany post Erica Lipperta:
https://blogs.msdn.microsoft.com/ericlippert/2009/06/11/what-does-the-optimize-switch-do/
a także artykuł Scotta Hanselmana:
http://www.hanselman.com/blog/ReleaseISNOTDebug64bitOptimizationsAndCMethodInliningInReleaseBuildCallStacks.aspx
w którym bardzo szczegółowo opisał on to, co chciałem opisać ja.
Skoro wszystko mamy w tych dwóch linkach, to tylko szybkie, dwupunktowe, podsumowanie.
1. Generowanie symboli
- Opcja kompilacji /debug włącza/wyłącza tworzenie danych do debugowania. Opcja ta jest automatycznie włączana przez VS w trybie „Debug”.
- Opcja kompilacji /optimize określa, czy kompilator ma dokonać optymalizacji kodu.
Poniżej przedstawiam fragment pliku *.csproj ukazujący domyślne ustawienia obu trybów:
Jak widać, w tym przypadku interesują nas głównie elementy DebugSymbols oraz DebugType. DebugType można ustawić przechodząc do ustawień projektu, a dalej Build -> Advanced…
Okno dialogowe VS Project settings->Build->Advanced… |
Są tam trzy opcje:
- none – pliki pdb nie będą tworzone(podstawowe wiadomości dot. pdb świetnie opisał Piotr Zieliński na swoim blogu: http://www.pzielinski.com/?p=1041)
- pdbonly – wygeneruje pliki pdb oraz zoptymalizowany kod bez atrybutu DebuggableAttribute
- full – wygeneruje pliki pdb oraz atrybut tak, aby możliwe było pełne debugowanie z podglądaniem wartości itp.
2. Optymalizacja
- Dodaniu atrybutu Debuggable z wyłączoną opcją IsJITOptimizerDisabled. Pozwala to JIT na optymalizację kodu, zmiany kolejności linii kodu oraz tworzenie tzw. inliningu czyli zwijania kodu. Przykładowo, zamiast wywołania metody Add, która dodaje dwie liczby, kompilator zmieni to wywołanie na dodawanie dwóch liczb. Optymalizuje to działanie kodu, ale bardzo utrudnia debugowanie (jak widzimy w artykule Scotta Hanselmana oraz na moim przykładzie)
- Analizie kodu i eliminowaniu kodu martwego (niedostępnego). Np. if(false)Execute(), będzie całkowicie usunięte.
- Usuwa z kodu IL instrukcje NOP (czyli „puste”), które umożliwiają ustawienie „pułapek” w kodzie (breakpoints).
Znaleziona ciekawostka
Po krótkim śledztwie dowiedziałem się, że przyczyną jest to, że TeamCity całą kompilację przeprowadza w trybie Release, a ja sprawdzałem kod w trybie Debug. Po przeczytaniu poprzedniego podrozdziału tego artykułu już wiadomo o co chodzi – optymalizacja… W trybie Debug wartość zmiennej actual wynosiła DebugUtilTests.Deep3, a w trybie Release: DebugUtilTests.GetCallerName_ReturnsProperName. Wiemy dobrze, z czego to wynika, otóż metody zagnieżdżone Deep2, Deep3 oraz Deep4 niczemu nie służą i dlatego kompilator zdecydował się na ich usunięcie (inlining).
Rozwiązaniem tego problemu było dodanie atrybutu
[MethodImpl(MethodImplOptions.NoOptimization)]
Wg Scotta Hanselmana możliwe jest też użycie w atrybucie flagi MethodImplOptions.NoInlining, co brzmi bardzo sensownie.
I to w sumie tyle – ot, taka przypominajka, że wszystko może pójść nie do końca tak jak zakładamy – czy to przez niewiedzę, czy niedopatrzenie.
Pisanie tego artykułu zaś przypomina, że wszystko o czym chcę napisać już zostało opisane i to dużo lepiej przez kogoś innego.