Czy istnieje różnica między tymi dwiema wersjami kodu?
foreach (var thing in things)
{
int i = thing.number;
// code using 'i'
// pay no attention to the uselessness of 'i'
}
int i;
foreach (var thing in things)
{
i = thing.number;
// code using 'i'
}
Czy to nie obchodzi kompilatora? Kiedy mówię o różnicy, mam na myśli wydajność i zużycie pamięci. ..A właściwie po prostu jakakolwiek różnica, czy może oba kończą się tym samym kodem po kompilacji?
c#
performance
memory
Alternatex
źródło
źródło
Odpowiedzi:
TL; DR - są to równoważne przykłady na warstwie IL.
DotNetFiddle sprawia, że odpowiedź jest ładna, ponieważ pozwala zobaczyć wynikową IL.
Użyłem nieco innej odmiany twojej konstrukcji pętli, aby przyspieszyć testowanie. Użyłem:
Wariant 1:
Wariant 2:
W obu przypadkach skompilowane wyjście IL renderowało to samo.
Aby odpowiedzieć na twoje pytanie: kompilator optymalizuje deklarację zmiennej i równoważy te dwie odmiany.
O ile mi wiadomo, kompilator .NET IL przenosi wszystkie deklaracje zmiennych na początek funkcji, ale nie mogłem znaleźć dobrego źródła, które wyraźnie stwierdziłoby, że 2 . W tym konkretnym przykładzie widać, że przesunęło to ich o następującą instrukcję:
W tym przypadku stajemy się zbyt obsesyjni w dokonywaniu porównań ...
Przypadek A: czy wszystkie zmienne są przenoszone w górę?
Aby zagłębić się w to, przetestowałem następującą funkcję:
Różnica polega na tym, że możemy zadeklarować OSOBĄ
int i
lubstring j
w oparciu o porównanie. Ponownie kompilator przenosi wszystkie zmienne lokalne na szczyt funkcji 2 za pomocą:Zauważyłem, że warto zauważyć, że chociaż
int i
nie zostanie zadeklarowany w tym przykładzie, kod do jego obsługi jest generowany.Przypadek B: A może
foreach
zamiastfor
?Wskazano, że
foreach
ma inne zachowaniefor
i że nie sprawdzałem tego, o co pytano. Wstawiłem więc te dwie sekcje kodu, aby porównać wynikową IL.int
deklaracja poza pętlą:int
deklaracja wewnątrz pętli:Powstała IL z
foreach
pętlą rzeczywiście różniła się od IL wygenerowanej za pomocąfor
pętli. W szczególności zmieniono blok inicjujący i sekcję pętli.foreach
Podejście bardziej zmienne generowane lokalnych i wymaga pewnych dodatkowych rozgałęzień. Zasadniczo za pierwszym razem przeskakuje na koniec pętli, aby uzyskać pierwszą iterację wyliczenia, a następnie przeskakuje z powrotem na prawie szczyt pętli, aby wykonać kod pętli. Następnie przechodzi przez pętlę, jak można się spodziewać.Ale poza różnicami rozgałęziających spowodowane użyciem
for
iforeach
konstrukcje, nie było bez różnicy w IL oparciu którymint i
zgłoszenie zostało złożone. Więc nadal jesteśmy przy dwóch podejściach równoważnych.Przypadek C: Co z różnymi wersjami kompilatora?
W komentarzu, który pozostawiono 1 , był link do pytania SO dotyczącego ostrzeżenia o zmiennym dostępie z foreach i zamykaniem . Część, która naprawdę przykuła moją uwagę w tym pytaniu, polegała na tym, że mogły istnieć różnice w działaniu kompilatora .NET 4.5 w porównaniu z wcześniejszymi wersjami kompilatora.
I właśnie tam zawiodła mnie witryna DotNetFiddler - mieli tylko .NET 4.5 i wersję kompilatora Roslyn. Więc przywołałem lokalną instancję Visual Studio i zacząłem testować kod. Aby upewnić się, że porównuję te same rzeczy, porównałem lokalnie zbudowany kod w .NET 4.5 z kodem DotNetFiddler.
Jedyną różnicą, którą zauważyłem, był lokalny blok init i deklaracja zmiennej. Lokalny kompilator był nieco bardziej szczegółowy w nazywaniu zmiennych.
Ale z tą niewielką różnicą było to do tej pory tak dobre. Miałem równoważne wyjście IL między kompilatorem DotNetFiddler a tym, co produkowała moja lokalna instancja VS.
Więc następnie przebudowałem projekt ukierunkowany na .NET 4, .NET 3.5 i dla pewności tryb wydania .NET 3.5.
I we wszystkich tych trzech dodatkowych przypadkach wygenerowana IL była równoważna. Wybrana wersja .NET nie miała wpływu na IL wygenerowaną w tych próbkach.
Podsumowując tę przygodę: Myślę, że możemy śmiało powiedzieć, że kompilator nie dba o to, gdzie deklarujesz typ prymitywny i że nie ma to żadnego wpływu na pamięć ani wydajność żadnej z metod deklaracji. I to obowiązuje niezależnie od użycia pętli
for
lubforeach
.Zastanawiałem się nad uruchomieniem kolejnej sprawy, która zawiera zamknięcie wewnątrz
foreach
pętli. Ale zapytałeś o skutki, gdzie zadeklarowano zmienną typu pierwotnego, więc pomyślałem, że sięgam zbyt daleko poza to, o co chciałeś zapytać. Pytanie SO, o którym wspomniałem wcześniej, ma świetną odpowiedź, która zapewnia dobry przegląd efektów zamknięcia na zmiennych iteracji foreach.1 Dziękujemy Andy'emu za dostarczenie oryginalnego linku do pytania SO dotyczącego zamykania
foreach
pętli.2 Warto zauważyć, że specyfikacja ECMA-335 rozwiązuje ten problem w sekcji I.12.3.2.2 „Zmienne lokalne i argumenty”. Musiałem zobaczyć wynikową IL, a następnie przeczytać sekcję, aby było jasne, co się dzieje. Dzięki grzechotnikowi za wskazanie tego na czacie.
źródło
foreach
pętli, a także sprawdziłem docelową wersję .NET.W zależności od używanego kompilatora (nawet nie wiem, czy C # ma więcej niż jeden), kod zostanie zoptymalizowany przed przekształceniem w program. Dobry kompilator zobaczy, że za każdym razem ponownie inicjujesz tę samą zmienną z inną wartością i efektywnie zarządzasz dla niej pamięcią.
Jeśli za każdym razem inicjalizujesz tę samą zmienną na stałą, kompilator również zainicjuje ją przed pętlą i odniesie do niej.
Wszystko zależy od tego, jak dobrze napisany jest Twój kompilator, ale jeśli chodzi o standardy kodowania, zmienne powinny zawsze mieć możliwie najmniejszy zakres. Więc deklarowanie wewnątrz pętli jest tym, czego zawsze mnie uczono.
źródło
na początku deklarujesz i inicjujesz pętlę wewnętrzną, więc za każdym razem pętla będzie ponownie inicjowana „i” pętli wewnętrznej. W drugiej chwili deklarujesz tylko poza pętlą.
źródło