2009-06-20 20 views
5

Próbowałem dowiedzieć się, czy pętla for była szybsza od pętli foreach i używała klas System.Diagnostics do wykonania zadania. Podczas wykonywania testu zauważyłem, że kiedykolwiek pierwsza pętla, którą stawiam, zawsze wykonuje wolniej niż ostatnia. Czy ktoś może mi powiedzieć, dlaczego tak się dzieje? Moje kodu jest poniżej:Dlaczego druga pętla for zawsze działa szybciej niż pierwsza?

using System; 
using System.Diagnostics; 

namespace cool { 
    class Program { 
     static void Main(string[] args) { 
      int[] x = new int[] { 3, 6, 9, 12 }; 
      int[] y = new int[] { 3, 6, 9, 12 }; 

      DateTime startTime = DateTime.Now; 
      for (int i = 0; i < 4; i++) { 
       Console.WriteLine(x[i]); 
      } 
      TimeSpan elapsedTime = DateTime.Now - startTime; 

      DateTime startTime2 = DateTime.Now; 
      foreach (var item in y) { 
       Console.WriteLine(item); 
      } 
      TimeSpan elapsedTime2 = DateTime.Now - startTime2; 

      Console.WriteLine("\nSummary"); 
      Console.WriteLine("--------------------------\n"); 
      Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2); 

      Console.ReadKey(); 
     } 
    } 
} 

Oto wynik:

for:   00:00:00.0175781 
foreach:  00:00:00.0009766 
+5

Wystarczy krótka notatka: Podczas rozrządu coś do określenia względnych czasów wykonania, nie robić nic wyjściowego (WriteLine()) wewnątrz pętli. Czas potrzebny na zrobienie WriteLine() jest prawdopodobnie tysiące (do milionów) razy dłuższy niż to, co próbujesz testować, więc tracisz poczucie dokładności. Ponadto będziesz potrzebował więcej niż czterech (4) iteracji, aby były znaczące. Wypróbuj tysiące (lub nawet miliony). –

+4

Jest tylko jedna prawdziwa odpowiedź na twoje pytanie: twój benchmark jest poważnie wadliwy. Inne wspomniane punkty są prawdziwe, ale nie dlatego otrzymujesz ten wynik. –

+0

Co to jest, jeśli najpierw wykonasz pętlę foreach? –

Odpowiedz

15

Prawdopodobnie dlatego zajęcia (np konsole) muszą być JIT skompilowane za pierwszym razem. Dostaniesz najlepsze metryki, wywołując wszystkie metody (JIT je (najpierw ciepłe, a następnie w górę)), a następnie wykonując test.

Jak wskazali inni użytkownicy, 4 przejścia nigdy nie będą wystarczające, aby pokazać różnicę.

Nawiasem mówiąc, różnica w wydajności między prognozą i foreach będzie znikoma, a korzyści z czytelności wynikające z używania foreach prawie zawsze przewyższają marginalną korzyść z wydajności.

2

Powodem jest tam kilka form napowietrznych w wersji foreach, które nie są obecne w pętli for

  • Używanie IDisposable.
  • Dodatkowa metoda wywołania dla każdego elementu. Każdy element musi być dostępny pod maską przy użyciu IEnumerator<T>.Current, która jest wywołaniem metody. Ponieważ jest w interfejsie, nie można go wstawić. Oznacza to, że N wywołuje metodę, gdzie N jest liczbą elementów w wyliczeniu. Pętla for właśnie używa i indeksuje
  • W pętli foreach wszystkie połączenia przechodzą przez interfejs. W ogóle to trochę wolniej niż przez konkretny typ

Należy pamiętać, że rzeczy wymienione powyżej są nie koniecznie ogromne koszty. Są to zazwyczaj bardzo małe koszty, które mogą przyczynić się do niewielkiej różnicy w wydajności.

Należy również zauważyć, jak zauważył Mehrdad, kompilatory i JIT mogą wybrać optymalizację pętli foreach dla pewnych znanych struktur danych, takich jak tablica. Końcowym rezultatem może być po prostu pętla for.

Uwaga: Twój benchmark wydajnościowy wymaga nieco więcej pracy, aby był dokładny.

  • Należy używać StopWatch zamiast DateTime. Jest o wiele bardziej dokładny w przypadku testów wydajności.
  • Powinieneś wykonać test wiele razy, nie tylko jeden raz,
  • Musisz wykonać sztuczny przebieg w każdej pętli, aby wyeliminować problemy, które przychodzą z JITing metody po raz pierwszy. Prawdopodobnie nie jest to problemem, gdy cały kod jest w tej samej metodzie, ale nie boli.
  • Musisz użyć więcej niż 4 wartości z listy. Zamiast tego spróbuj 40 000.
+0

Przepraszam, nie byłam jasna. Jeśli ustawię pętlę foreach na sekundę, wykonam ją szybciej niż pętlę for. –

+0

@JaredPar: pętla foreach dla tablic jest zoptymalizowana przez kompilator. –

+0

Kolejnym elementem do dodania do listy jest zmiana priorytetu wątku na coś wyższego, aby zminimalizować interferencję innych wątków działających na tym samym procesorze. – RichardOD

7
  1. Nie użyłbym DateTime do pomiaru wydajności - spróbuj klasę Stopwatch.
  2. Pomiar z tylko 4 przejściami nigdy nie przyniesie dobrego wyniku. Lepsze użycie> 100 000 przebiegów (możesz użyć zewnętrznej pętli). Nie rób w swojej pętli Console.WriteLine.
  3. Nawet lepiej: użyj profilera (jak RedGate mrówek lub być może NProf)
+0

Czy możesz podać kilka linków dotyczących korzystania z klasy stopera? –

+4

Stopwatch Class (przez MSDN): http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx –

+0

Dzięki Robert ... – tanascius

1

Powinieneś używać StopWatch do timeowania zachowania.

Technicznie, pętla dla jest szybsza. Foreach wywołuje metodę MoveNext() (tworząc stos metod i inne obciążenie z połączenia) w iteratorze IEnumerable, gdy dla tylko musi zwiększyć wartość zmiennej.

3

Nie jestem w C#, ale kiedy dobrze pamiętam, Microsoft budował kompilatory "Just in Time" dla Javy. Kiedy używają tych samych lub podobnych technik w języku C#, byłoby raczej naturalne, że "niektóre konstrukcje, które są na drugim miejscu, działają szybciej".

Przykładowo może się zdarzyć, że system JIT zauważy, że wykonywana jest pętla i decyduje się na kompilację całej metody. Dlatego po osiągnięciu drugiej pętli jest ona jeszcze skompilowana i działa znacznie szybciej niż pierwsza. Jest to jednak dość uproszczony domysł. Oczywiście potrzebujesz znacznie lepszego wglądu w system runtime C#, aby zrozumieć, co się dzieje. Może się również zdarzyć, że strona RAM jest dostępna najpierw w pierwszej pętli, a w drugiej wciąż znajduje się w pamięci podręcznej procesora.

Addon: Drugi komentarz, który został napisany: że moduł wyjściowy może być JITed po raz pierwszy w pierwszych pętlach pętli dla mnie bardziej prawdopodobny niż moje pierwsze przypuszczenie. Współczesne języki są bardzo skomplikowane, aby dowiedzieć się, co dzieje się pod maską. Również to stwierdzenie pasuje do tego:

Ale również masz końcowe wyjścia w swoich pętlach. Sprawiają, że rzeczy jeszcze trudniejsze. Może się również zdarzyć, że za pierwszym razem pojawi się w programie jakiś czas.

3

Właśnie przeprowadzałem testy, aby uzyskać prawdziwe liczby, ale w międzyczasie Gaz pokonał mnie do odpowiedzi - połączenie z Console.Writeline jest podważane podczas pierwszego połączenia, więc płacisz ten koszt w pierwszej pętli.

tylko w celach informacyjnych, choć - za pomocą stopera zamiast datetime i mierząc liczbę kleszczy:

bez zaproszenia do Console.WriteLine przed pierwszą pętlą czasy były

 
for: 16802 
foreach: 2282 

z rozmowy do Console.WriteLine byli

 
for: 2729 
foreach: 2268 

Choć wyniki te nie były konsekwentnie powtarzalne ze względu na ograniczoną liczbę przebiegów, ale T Różnica wielkości zawsze była mniej więcej taka sama.


Kod edytowany przez odniesienie:

 int[] x = new int[] { 3, 6, 9, 12 }; 
     int[] y = new int[] { 3, 6, 9, 12 }; 

     Console.WriteLine("Hello World"); 

     Stopwatch sw = new Stopwatch(); 

     sw.Start(); 
     for (int i = 0; i < 4; i++) 
     { 
      Console.WriteLine(x[i]); 
     } 
     sw.Stop(); 
     long elapsedTime = sw.ElapsedTicks; 

     sw.Reset(); 
     sw.Start(); 
     foreach (var item in y) 
     { 
      Console.WriteLine(item); 
     } 
     sw.Stop(); 
     long elapsedTime2 = sw.ElapsedTicks; 

     Console.WriteLine("\nSummary"); 
     Console.WriteLine("--------------------------\n"); 
     Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2); 

     Console.ReadKey(); 
+1

Gdybym mógł głosować 100 razy, zrobiłbym. To jest odpowiedź. Tak więc, 2 problemy - 1. używanie writeline w sekcji kodu, którą "profilujesz" 2. JIT jest winowajcą różnic. – Tim

+0

@ boat-programmer: Seconded! – Kredns

1

nie widzę dlaczego wszyscy tutaj mówi, że for byłoby szybciej niż foreach w tym konkretnym przypadku.W przypadku modelu List<T> jest on (o około dwa razy wolniejszy w stosunku do wartości foreach niż w przypadku listy niż w przypadku for za pośrednictwem List<T>).

Rzeczywiście, foreach będzie nieco szybsza niż tutaj for tutaj. Ponieważ foreach na tablicy zasadniczo kompiluje do:

for(int i = 0; i < array.Length; i++) { } 

Korzystanie .Length jako kryterium stopu umożliwia JIT usunąć granice kontroli dostępu do tablicy, ponieważ jest to przypadek szczególny. Użycie i < 4 powoduje, że JIT wstawia dodatkowe instrukcje sprawdzania każdej iteracji niezależnie od tego, czy i jest poza granicami tablicy, i w takim przypadku generuje wyjątek. Jednak z .Length, może zagwarantować, że nigdy nie wyjdziesz poza granice tablicy, więc kontrole graniczne są zbędne, dzięki czemu jest szybszy.

Jednak w większości pętli, narzut pętli jest nieznaczny w porównaniu do pracy wykonanej w środku.

Widoczne rozbieżności można wytłumaczyć tylko za pomocą JIT.

1

Nie przeczytałbym zbyt wiele na ten temat - nie jest to dobry kod profilowania z następujących powodów:
1. DateTime nie jest przeznaczony do profilowania. Powinieneś użyć QueryPerformanceCounter lub StopWatch, które używają liczników sprzętu sprzętowego CPU
2. Console.WriteLine jest metodą urządzenia, więc mogą występować subtelne efekty, takie jak buforowanie, aby wziąć pod uwagę
3. Uruchomienie jednej iteracji każdego bloku kodu nigdy nie będzie daje dokładne wyniki, ponieważ twój procesor ma wiele funky w locie optymalizacji, takich jak poza wykonanie zamówienia i planowanie instrukcji
4. Istnieje szansa, że ​​kod, który dostaje JIT dla obu bloków kodu jest dość podobny, więc prawdopodobnie będzie w Pamięć podręczna instrukcji dla drugiego bloku kodu

Aby uzyskać lepszy pomysł na synchronizację czasu, zrobiłem co następuje:

  1. Zastępuje się Console.WriteLine pomocą wyrażenia matematyczne (e^Lb)
  2. kiedyś QueryPerformanceCounter/QueryPerformanceTimer przez P/Wywołanie
  3. uruchomiony każdy blok kodowy 1 milion razy, a następnie uśredniano te wyniki

Kiedy zrobiłam, że mam następujące wyniki:

pętli for wziął 0.000676 milisekund
pętla foreach zajęło 0.000653 milisekund

Więc foreach bardzo nieznacznie szybciej, ale nie za dużo

I potem zrobił kilka dalszych eksperymentów i prowadził pierwszy blok foreach i dla bloku drugiego
Kiedy zrobiłam, że mam następujące wyniki:

Pętla foreach zajęła 0.000702 milisekundy
Pętla for trwała 0.000691 milisekund

W końcu uruchomiłem obie pętle razem dwa razy.e Za + foreach następnie przez + foreach ponownie
Kiedy zrobiłam, że mam następujące wyniki:

Pętla foreach miały 0.00140 milisekund
Pętla for miały 0.001385 milisekund

Więc w zasadzie to wygląda mi się, że jakikolwiek kod, który uruchomisz na drugim miejscu, działa bardzo nieznacznie szybciej, ale nie na tyle, aby mieć jakiekolwiek znaczenie.
--Edit--
Oto kilka przydatnych linków
How to time managed code using QueryPerformanceCounter
The instruction cache
Out of order execution

Powiązane problemy