@somekind, @fasadin
var array = new int[100000000];
var sw = new Stopwatch();
// tu wstępna rozgrzewka, żeby zminimalizować wpływ kolejności wykonywania pętli, bez tego fragmentu pierwsza pętla wykonuje się prawie dwa razy dłużej
for (int i = 0; i < array.Length; i++) DoSomething(array[i]);
// var
sw.Restart();
var l = array.Length;
for (int i = 0; i < l; i++) DoSomething(array[i]);
Console.WriteLine(sw.Elapsed);
// inline
sw.Restart();
for (int i = 0; i < array.Length; i++) DoSomething(array[i]);
Console.WriteLine(sw.Elapsed);
// count
sw.Restart();
for (int i = 0; i < array.Count(); i++) DoSomething(array[i]);
Console.WriteLine(sw.Elapsed);
Wynik nie potwierdza tego, że Length inline będzie szybszy. .NET 4.5.1, x64, w zależności od kolejności sposobu var
i inline
:
release z rozgrzewką, kolejność v-i-c:
00:00:00.2081505 - var 00:00:00.2020022 - inline 00:00:04.3296516 - count - jak widać wersja z Count() jest najwolniejsza
release z rozgrzewką, kolejność i-v-c:
00:00:00.2010472 - inline 00:00:00.2028995 - var 00:00:04.4164176 - count
release z rozgrzewką, kolejność c-i-v:
00:00:04.3482894 - count 00:00:00.2038560 - inline 00:00:00.1712872 - var
release z rozgrzewką, kolejność c-v-i:
00:00:04.3437966 - count 00:00:00.2023574 - var - szybciej niż inline w poprzednim przypadku (!) 00:00:00.1818407 - inline - wolniej niż var w poprzednim przypadku (!)
Jeszcze jako ciekawostka:
debug z rozgrzewką, kolejność c-v-i:
00:00:04.6194327 - count - minimalny wpływ przełączenia z release na debug 00:00:00.4090494 - var - około dwa razy dłużej niż release, ALE tym razem drugi algorytm zawsze wygrywa z trzecim, czyli w debug użycie zmiennej jest szybsze od odwołania do property 00:00:00.4399661 - inline
release bez rozgrzewki
00:00:00.2747313 - var 00:00:00.1765430 - inline
i na odwrót
00:00:00.2746089 - inline 00:00:00.1709781 - var
debug z unchecked (słowo kluczowe unchecked
) i rozgrzewką
00:00:00.2498583 - inline 00:00:00.2449227 - var
release z unchecked i rozgrzewką
00:00:00.2415720 - inline 00:00:00.2481471 - var
j/w, odwrotna kolejność:
00:00:00.2435900 - var - tu odwrotny wniosek, var jest o kilka promili wolniejszy 00:00:00.2499914 - inline - a tu z kolei też wolniej
Wnioski: wpływ kolejności wykonywania pętli jest zadziwiająco duży, im później algorytm się wykonuje tym ma większe fory, a przecież tablica ma 400MB, więc cache L1-L3 nie powinien mieć nic do tego. Z tego powodu tyle testów.
Porównując czasy wykonania algorytmów wykonywanych na tych samych miejscach wersja z użyciem dodatkowej zmiennej (nazwałem ją sobie "var") jest zwykle minimalnie (0-5%) szybsza od użycia odwołanie do pola bezpośrednio w pętli ("count"). Czyli albo boundary check nie ma nic do rzeczy, albo mój kompilator ssie (ale w release build ma włączoną optymalizację). Użycie metody Count() zwiększa czas wykonania o rząd wielkości (!), jak widać nie ma tu żadnej optymalizacji i metoda jest wołana przy każdej iteracji.
Widać też, na czym polega w tym konkretnym przypadku optymalizacja kompilatora przy buildzie dla release - wyłączany jest boundary check.
Trzeba wziąć pod uwagę, że sama pętla zwykle w swoim ciele ma zawartą operację znacznie bardziej czasochłonną od samej pętli, wtedy to, co jest zawarte w warunku zakończenia pętli jest drugorzędne.
Zrobiłem jeszcze mały test dla starszych .net i oczy mi się szeroko otworzyły:
2.0: 0000.0720307 - ponad trzy razy szybciej - optymalizator wyciął wywołanie DoSomething
3.0: 0000.0703629 - j/w
3.5: 0000.0647380 - j/w
3.5 CP: 0000.0600336 - (!) to już cztery razy szybciej
4.0: 0000.2273934 (prawie 10% szybciej bez wycinania DoSomething)
4.6.1 dla x86: 0000.1941212
4.6.1 dla x64: 0000.2357737
4.6.1 dla any cpu: wyniki jak dla x64
Na koniec jeszcze pstryczek w nos @_13th_Dragon: wersja z preinkrementacją w pętlach (release, unchecked z rozgrzewką) - jak widać czasy są takie same, jak w postinkrementacji:
00:00:00.2463280 - inline 00:00:00.2499132 - var