Różnice w wydajności między wersjami debugowania i wersji


280

Muszę przyznać, że zwykle nie kłopotałem się przełączaniem między konfiguracjami debugowania i wydania w moim programie i zwykle decydowałem się na konfigurację debugowania , nawet jeśli programy są faktycznie wdrażane u klientów.

O ile mi wiadomo, jedyną różnicą między tymi konfiguracjami, jeśli nie zmienisz go ręcznie, jest to, że Debug ma DEBUGzdefiniowaną stałą, a Release ma zaznaczony kod Optimize .

Tak więc moje pytania są dwojakie:

  1. Czy występują duże różnice wydajności między tymi dwiema konfiguracjami. Czy jest jakiś konkretny rodzaj kodu, który spowoduje tutaj duże różnice w wydajności, czy faktycznie nie jest to tak ważne?

  2. Czy istnieje jakiś rodzaj kodu, który będzie działał poprawnie w konfiguracji debugowania, który może się nie powieść w konfiguracji wydania , czy możesz być pewien, że kod, który jest testowany i działa poprawnie w konfiguracji debugowania, będzie również działał dobrze w konfiguracji wydania.


Odpowiedzi:


511

Sam kompilator C # nie zmienia zbyt wiele emitowanej IL w kompilacji Release. Godne uwagi jest to, że nie emituje już kodów NOP, które pozwalają ustawić punkt przerwania na nawiasach klamrowych. Duży to optymalizator wbudowany w kompilator JIT. Wiem, że dokonuje następujących optymalizacji:

  • Metoda inliningu. Wywołanie metody jest zastępowane przez wstrzyknięcie kodu metody. Jest to duży, sprawia, że ​​dostęp do nieruchomości jest zasadniczo bezpłatny.

  • Przydział rejestru procesora. Lokalne zmienne i argumenty metody mogą pozostać przechowywane w rejestrze procesora bez (lub rzadziej) przechowywania z powrotem do ramki stosu. Jest to duży problem, szczególnie utrudniający debugowanie zoptymalizowanego kodu. I nadając zmiennemu słowu kluczowemu znaczenie.

  • Eliminacja sprawdzania indeksu tablicy. Ważna optymalizacja podczas pracy z tablicami (wszystkie klasy kolekcji .NET używają tablicy wewnętrznie). Gdy kompilator JIT może sprawdzić, czy pętla nigdy nie indeksuje tablicy poza granicami, eliminuje to sprawdzanie indeksu. Duży.

  • Rozwijanie pętli. Pętle z małymi ciałami są ulepszane przez powtarzanie kodu do 4 razy w ciele i mniej zapętlania. Zmniejsza koszt oddziału i poprawia super-skalarne opcje wykonania procesora.

  • Eliminacja martwego kodu. Instrukcja taka jak if (false) {/ ... /} zostanie całkowicie wyeliminowana. Może się to zdarzyć z powodu ciągłego składania i wkładania. W innych przypadkach kompilator JIT może ustalić, że kod nie ma możliwego efektu ubocznego. Ta optymalizacja sprawia, że ​​profilowanie kodu jest tak trudne.

  • Podnoszenie kodu. Kod wewnątrz pętli, na który pętla nie ma wpływu, można przenieść z pętli. Optymalizator kompilatora C poświęci znacznie więcej czasu na znalezienie okazji do podniesienia. Jest to jednak kosztowna optymalizacja ze względu na wymaganą analizę przepływu danych, a jitter nie może sobie pozwolić na czas, więc podnoszą tylko oczywiste przypadki. Zmuszanie programistów .NET do pisania lepszego kodu źródłowego i podnoszenia się.

  • Wspólna eliminacja podwyrażeń. x = y + 4; z = y + 4; staje się z = x; Dość powszechne w instrukcjach takich jak dest [ix + 1] = src [ix + 1]; napisane dla czytelności bez wprowadzania zmiennej pomocniczej. Nie trzeba zmniejszać czytelności.

  • Stałe składanie. x = 1 + 2; staje się x = 3; Ten prosty przykład został wcześnie wychwycony przez kompilator, ale dzieje się to w czasie JIT, kiedy inne optymalizacje umożliwiają to.

  • Kopiuj propagację. x = a; y = x; staje się y = a; Pomaga to alokatorowi rejestru podejmować lepsze decyzje. W jitteru x86 jest to wielka sprawa, ponieważ ma niewiele rejestrów do pracy. Wybór odpowiednich jest kluczowy dla perf.

Są to bardzo ważne optymalizacje, które mogą spowodować wielki kontrakt różnicy kiedy, na przykład, profil kompilacji Debug aplikacji i porównać go do kompilacji Release. To naprawdę ma znaczenie tylko wtedy, gdy kod znajduje się na twojej krytycznej ścieżce, 5 do 10% pisanego kodu, które faktycznie wpływa na perf twojego programu. Optymalizator JIT nie jest wystarczająco inteligentny, aby z góry wiedzieć, co jest najważniejsze, może zastosować tylko pokrętło „zamień na jedenaście” dla całego kodu.

Na efektywny wynik tych optymalizacji czasu wykonywania programu często wpływa kod działający w innym miejscu. Odczytywanie pliku, wykonywanie zapytania dbase itp. Optymalizacja JIT czyni pracę całkowicie niewidoczną. Nie przeszkadza to jednak :)

Optymalizator JIT jest dość niezawodnym kodem, głównie dlatego, że został przetestowany miliony razy. Bardzo rzadko występują problemy z wersją kompilacji wydania programu. Tak się jednak zdarza. Zarówno jitter x64, jak i x86 miały problemy ze strukturami. Jitter x86 ma problemy ze spójnością zmiennoprzecinkową, powodując nieznacznie różne wyniki, gdy półprodukty obliczeń zmiennoprzecinkowych są przechowywane w rejestrze FPU z 80-bitową precyzją, zamiast zostać obciętym po opróżnieniu do pamięci.


23
Nie sądzę, że wszystkie kolekcje używają tablic: LinkedList<T>nie, chociaż nie są używane zbyt często.
svick

Myślę, że CLR konfiguruje FPU z 53-bitową precyzją (dopasowanie podwójnych szerokości 64-bitowych), więc nie powinno być 80-bitowych rozszerzonych podwójnych obliczeń dla wartości Float64. Jednak obliczenia Float32 mogą być obliczane z tą 53-bitową precyzją i tylko obcinane, gdy są przechowywane w pamięci.
Govert

2
Słowo volatilekluczowe nie dotyczy zmiennych lokalnych przechowywanych w ramce stosu. Z dokumentacji na msdn.microsoft.com/en-us/library/x13ttww7.aspx : „Zmienne słowo kluczowe można zastosować tylko do pól klasy lub struktury. Zmiennych lokalnych nie można uznać za zmienne”.
Kris Vandermotten

8
jako skromna poprawka, myślę, że to, co naprawdę robi różnicę Debugi Releasebuduje pod tym względem, to pole wyboru „optymalizuj kod”, które jest zwykle włączone, Releaseale wyłączone Debug. Tylko po to, aby czytelnicy nie zaczęli myśleć, że istnieją „magiczne”, niewidoczne różnice między dwiema konfiguracjami kompilacji, które wykraczają poza to, co można znaleźć na stronie właściwości projektu w Visual Studio.
chiccodoro

3
Być może warto wspomnieć, że praktycznie żadna z metod w System.Diagnostics.Debug nie robi nic w kompilacji debugowania. Zmienne również nie są tak szybko finalizowane ( stackoverflow.com/a/7165380/20553 ).
Martin Brown,

23
  1. Tak, istnieje wiele różnic wydajności, które naprawdę dotyczą całego kodu. Debugowanie bardzo mało optymalizuje wydajność, a tryb zwolnienia bardzo;

  2. Tylko kod oparty na DEBUGstałej może działać inaczej z kompilacją wydania. Poza tym nie powinieneś widzieć żadnych problemów.

Przykładem kodu frameworka zależnego od DEBUGstałej jest Debug.Assert()metoda z [Conditional("DEBUG)"]zdefiniowanym atrybutem . Oznacza to, że zależy to również od DEBUGstałej i nie jest to uwzględnione w kompilacji wydania.


2
To wszystko prawda, ale czy mógłbyś kiedykolwiek zmierzyć różnicę? Czy zauważysz różnicę podczas korzystania z programu? Oczywiście nie chcę nikogo zachęcać do wydania oprogramowania w trybie debugowania, ale pytanie brzmiało, czy istnieje ogromna różnica w wydajności i tego nie widzę.
testalino

2
Warto również zauważyć, że wersje do debugowania korelują z oryginalnym kodem źródłowym w znacznie większym stopniu niż wersje. Jeśli sądzisz (choć mało prawdopodobne), że ktoś może spróbować dokonać inżynierii wstecznej twoich plików wykonywalnych, nie chcesz ułatwiać im wdrażania wdrożonych wersji debugowania.
jwheron

2
@testalino - Cóż, w dzisiejszych czasach jest to trudne. Procesory stały się tak szybkie, że użytkownik prawie nie czeka na proces, aby faktycznie wykonać kod z powodu akcji użytkownika, więc wszystko to jest względne. Jeśli jednak wykonujesz jakiś długi proces, tak, zauważysz. Kod następujący przykład prowadzi 40% mniejsza przy DEBUG: AppDomain.CurrentDomain.GetAssemblies().Sum(p => p.GetTypes().Sum(p1 => p1.GetProperties().Length)).
Pieter van Ginkel

2
Ponadto, jeśli jesteś włączony asp.neti używasz debugowania zamiast wydania, na stronie mogą zostać dodane skrypty, takie jak: MicrosoftAjax.debug.jszawierający około 7 tys. Linii.
BrunoLM,

13

Zależy to w dużej mierze od charakteru twojej aplikacji. Jeśli twoja aplikacja jest obciążona interfejsem użytkownika, prawdopodobnie nie zauważysz żadnej różnicy, ponieważ użytkownik jest najwolniejszym komponentem podłączonym do nowoczesnego komputera. Jeśli korzystasz z niektórych animacji interfejsu użytkownika, możesz sprawdzić, czy możesz zauważyć zauważalne opóźnienie podczas uruchamiania w kompilacji DEBUG.

Jeśli jednak masz wiele obliczeń obciążających obliczenia, zauważysz różnice (może wynosić nawet 40%, jak wspomniano w @Pieter, choć będzie to zależało od charakteru obliczeń).

Zasadniczo jest to kompromis projektowy. Jeśli wydajesz w wersji DEBUG, to jeśli użytkownicy napotkają problemy, możesz uzyskać bardziej znaczące śledzenie i możesz wykonać bardziej elastyczną diagnostykę. Wydając w wersji DEBUG, unikasz również optymalizatora produkującego niejasne Heisenbugs .


11
  • Z mojego doświadczenia wynika, że ​​średnie lub większe aplikacje są zauważalnie bardziej responsywne w wersji Release. Wypróbuj aplikację i przekonaj się, jak to jest.

  • Jedną z rzeczy, które mogą cię ugryźć w kompilacjach wersji, jest to, że kod kompilacji debugowania może czasami tłumić warunki wyścigu i inne błędy związane z wątkami. Zoptymalizowany kod może spowodować zmianę kolejności instrukcji, a szybsze wykonanie może zaostrzyć niektóre warunki wyścigu.


9

Nigdy nie należy wypuszczać wersji .NET Debug do wersji produkcyjnej. Może zawierać brzydki kod do obsługi Edycji i Kontynuacji lub kto wie co jeszcze. O ile mi wiadomo, dzieje się tak tylko w VB, a nie w C # (uwaga: oryginalny post jest oznaczony jako C #) , ale nadal powinien dawać powód do wstrzymania się z tym, co Microsoft uważa, że ​​mogą robić z wersją debugowania. W rzeczywistości przed wersją .NET 4.0 kod VB przecieka pamięć proporcjonalnie do liczby instancji obiektów ze zdarzeniami, które konstruujesz w celu obsługi edycji i kontynuowania. (Chociaż zgłoszono, że jest to naprawione według https://connect.microsoft.com/VisualStudio/feedback/details/481671/vb-classes-with-events-are-not-garbage-collected-when-debugging , wygenerowany kod wygląda paskudnie, tworząc WeakReferenceobiekty i dodając je do statycznej listyz blokadą) Z pewnością nie chcę tego rodzaju wsparcia debugowania w środowisku produkcyjnym!


Wydawałem wersje debugowania wiele razy i nigdy nie widziałem problemu. Być może jedyną różnicą jest to, że nasza aplikacja po stronie serwera nie jest aplikacją internetową obsługującą wielu użytkowników. Ale jest to aplikacja po stronie serwera z bardzo dużym obciążeniem przetwarzania. Z mojego doświadczenia wynika, że ​​różnica między debugowaniem a wydaniem wydaje się całkowicie teoretyczna. Nigdy nie widziałem żadnej praktycznej różnicy w żadnej z naszych aplikacji.
Sam Goldberg,

5

Z mojego doświadczenia najgorsze, co wyszło z trybu Release, to niejasne „błędy wydania”. Ponieważ IL (język pośredni) jest zoptymalizowany w trybie Release, istnieje możliwość wystąpienia błędów, które nie pojawiłyby się w trybie Debugowania. Istnieją inne pytania SO dotyczące tego problemu: Typowe przyczyny błędów w wersji nie występują w trybie debugowania

Zdarzyło mi się to raz lub dwa razy, gdy prosta aplikacja konsolowa działałaby doskonale w trybie debugowania, ale przy tych samych danych wejściowych błąd występowałby w trybie zwolnienia. Te błędy są BARDZO trudne do debugowania (z definicji tryb Release, jak na ironię).


Aby śledzić, oto artykuł, który podaje przykład błędu w wydaniu: codeproject.com/KB/trace/ReleaseBug.aspx
Roly

Nadal jest to problem, jeśli aplikacja jest testowana i zatwierdzana z ustawieniami debugowania, nawet jeśli pomija błędy, jeśli powoduje to kompilację wydania podczas instalacji.
Øyvind Bråthen

4

Powiedziałbym, że 1) w dużej mierze zależy od twojego wdrożenia. Zwykle różnica nie jest tak duża. Wykonałem wiele pomiarów i często nie widziałem różnicy. Jeśli używasz niezarządzanego kodu, wielu ogromnych tablic i tym podobnych, różnica w wydajności jest nieco większa, ale nie inny świat (jak w C ++). 2) Zwykle w kodzie wydania wyświetlanych jest mniej błędów (wyższa tolerancja), dlatego przełącznik powinien działać poprawnie.


1
W przypadku kodu związanego z operacjami we / wy kompilacja wydania może nie być szybsza od debugowania.
Richard

0
    **Debug Mode:**
    Developer use debug mode for debugging the web application on live/local server. Debug mode allow developers to break the execution of program using interrupt 3 and step through the code. Debug mode has below features:
   1) Less optimized code
   2) Some additional instructions are added to enable the developer to set a breakpoint on every source code line.
   3) More memory is used by the source code at runtime.
   4) Scripts & images downloaded by webresource.axd are not cached.
   5) It has big size, and runs slower.

    **Release Mode:**
    Developer use release mode for final deployment of source code on live server. Release mode dlls contain optimized code and it is for customers. Release mode has below features:
   1) More optimized code
   2) Some additional instructions are removed and developer cant set a breakpoint on every source code line.
   3) Less memory is used by the source code at runtime.
   4) Scripts & images downloaded by webresource.axd are cached.
   5) It has small size, and runs fast.

2
wydaje się, że w trybie wydawania czasami pierwsze elementy listy nie są poprawnie ponumerowane. Również niektóre elementy na liście są duplikowane. :)
Gian Paolo
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.