Weryfikacja wyników mierzenia czasu wykonywania pętli

0

Cześć,
stworzyłem sobie taki o to kod:

class Program
    {
        static void Main(string[] args)
        {
            var list0 = Generate();
            var list1 = Generate();
            var list2 = Generate();
            var list3 = Generate();


            var watch0 = new Stopwatch();
            var watch1 = new Stopwatch();
            var watch2 = new Stopwatch();
            var watch3 = new Stopwatch();

            watch0.Start();
            for (var i = 0; i < 100000; i++)
            {
                list0[i] = list0[i] + "Changed";
            }
            watch0.Stop();
            Console.WriteLine("Time0: {0}", watch0.Elapsed.TotalSeconds);


            watch1.Start();
            for (var i = 0; i < 100000; i++)
            {
                list1[i] = list1[i] + "Changed";
            }
            watch1.Stop();
            Console.WriteLine("Time1: {0}", watch1.Elapsed.TotalSeconds);


            watch2.Start();
            for (var i = 0; i < 100000; i++)
            {
                list2[i] = list2[i] + "Changed";
            }
            watch2.Stop();
            Console.WriteLine("Time2: {0}", watch2.Elapsed.TotalSeconds);


            watch3.Start();
            for (var i = 0; i < 100000; i++)
            {
                list3[i] = list3[i] + "Changed";
            }
            watch3.Stop();
            Console.WriteLine("Time3: {0}", watch3.Elapsed.TotalSeconds);


            Console.Read();
        }

        public static List<string> Generate()
        {
            var list = new List<string>();
            for (int i = 0; i < 100000; i++)
            {
                list.Add(Guid.NewGuid().ToString());
            }
            return list;
        }         
    }

Czasy:
Time0: 0.0505036
Time1: 0.0133944
Time2: 0.0253824
Time3: 0.0245541

Czemu te czasy się tak między sobą różnią?

0

Czasy 1-3 są w miarę podobne. widocznie podczas pierwszego testu system 'postanowił' cos zrobić 'na boku'.
Wyłacz wszystkie inne aplikacje i spróbuj jeszcze raz.

0

przy pierwszym wywolaniu nastepuje kompilacja just-in-time i to ona mogla wplynac na wyniki. dobra praktyka jest jednorazowe wywolanie metod ktorych wydajnosc mierzymy i/lub pominiecie wynikow pierwszego (z np 10) pomiarow.

0

Skąd się bierze moje pytanie. Mianowicie stąd, że chciałem sprawdzić jaka funkcja (for czy foreach) i w jakiej konfiguracji będzie najsprawniejsza.
Przykładowo:

for(int i=0; i<100; i++)
{
    // kod
}

mogę tutaj użyć INT zamiast VAR (to pierwsze pytanie które będzie szybsze) i potem zamiast ustawiać zakres np. na 100, użyć Count albo Length na liście/tablicy.
Dodatkowo, w ogóle zamiast FOR mógłbym użyć właśnie FOREACH.

3

Zbyt mała próba - rozszerz zestaw danych tak żeby wykonanie trwało co najmniej sekundę, wpływ na czas mają wszystkie procesy działające w tle

var od int się niczym nie różni na etapie runtime - chodzi o to że podczas kompilacji typ jest wnioskowany, ale nie ma to wpływu na kod wynikowy ani tym bardziej na prędkość działania

foreach zawsze będzie wolniejszy od for bo ma dodatkowy narzut, ale w większości przypadków nie powinieneś się tym przejmować - możesz się przejąć np jeśli maszforeach wywoływany tysiące razy w pętli w każdej klatce podczas renderingu - wtedy faktycznie różnica może być na tyle znacząca że wpłynie na powiedzmy framerate gry

Możesz dać Thread.Sleep przed pętlami tak żeby program zdążył się "ustabilizować", możesz też wydzielić funkcje które mierzysz i raz je uruchomić przed pomiarem tak żeby JIT compiler zdążył sobie wszystko przetworzyć

0

var od int się niczym nie różni na etapie runtime - chodzi o to że podczas kompilacji typ jest wnioskowany, ale nie ma to wpływu na kod wynikowy ani tym bardziej na prędkość działania

Czyli jak sobie programuje to wstawiam "var" aby było szybciej itp. Ale podczas kompilacji (czy to asp.net mvc czy zwykłej konsolówki) kompilator podstawia odpowiednie typy za "var", tak?

1

Jeśli już musisz tak robić, to tak jak ktoś już pisał - zwiększ czasy, np. poprzez zwiększenie liczby operacji, coś w stylu:

 
watch0.Start();
for (uint j = 0; j < Int32.MaxValue; j++)
{
            for (var i = 0; i < 100000; i++)
            {
                list0[i] = list0[i] + "Changed";
            }
}
watch0.Stop();

Druga sprawa - jak już robisz sobie jakieś metody Generate() , i używasz potem zakresów elementów w tablicach np. od 1-100000, to ustal sobie jakieś maksymalne wartości w postaci zmiennej/stałej, a metody korzystające z obiektów z tablicami/listami wywołuj z parametrem określającym zakres tablicy (teraz by sobie zmienić 100000 na 22222 musisz to zmieniać w kilku miejscach, wiem, że w tym przypadku to nie jest aż tak krytyczne, ale ... plansza powinna mieć dowolny wymiar, a liczba graczy nie może być stała :D ).

1
ne0 napisał(a):

Czyli jak sobie programuje to wstawiam "var" aby było szybciej itp. Ale podczas kompilacji (czy to asp.net mvc czy zwykłej konsolówki) kompilator podstawia odpowiednie typy za "var", tak?

Tak. To się nazywa inferencja typów. Var to tylko słowo kluczowe, a nie typ.

Jak operujesz na tablicy w pętli for, to stosuj Length, a nie jakąś stałą, bo tak jest wydajniej.

1
somekind napisał(a)

Dobrą praktyką jest też przeczytanie kodu. :) On tu nie mierzy czasu wykonywania żadnej metody. - somekind wczoraj, 20:32

@somekind - indekser listy to tez metoda, a przy benchmarku obejmującym aż 0.01 sekundy to może mieć wpływ na wyniki. I @katelx ma racje, przy benchmarkowaniu zawsze sie pierwsze kilka wywołań pomija

foreach zawsze będzie wolniejszy od for bo ma dodatkowy narzut, ale w większości przypadków nie powinieneś się tym przejmować

Nieprawda, przy iterowaniu po np. tablicy albo liście foreach kompiluje się do dokładnie tego samego co for (po JITowaniu, nie chodzi mi o CIL). Teoretycznie może nawet być szybszy przy identycznym na pierwszy rzut oka kodzie, bo daje mocniejsze gwarancje (wymaga żeby kolekcja nie zmieniła się podczas iterowania, podczas gdy for tego nie oczekuje).

Możesz dać Thread.Sleep przed pętlami tak żeby program zdążył się "ustabilizować", możesz też wydzielić funkcje które mierzysz i raz je uruchomić przed pomiarem tak żeby JIT compiler zdążył sobie wszystko przetworzyć

JIT tak nie działa, nie skompiluje czegoś co się nie wykona. Trzeba tak jak @katelx pisze, wykonać funkcje zanim zostanie zJITowana.

Za to teoretycznie GC mógłby wpłynąć na wyniki, więc mozna dodać (z pamięci):

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

A, i tak jak inni piszą, zwiększ czasy (najlepiej żeby co najmniej kilka sekund kazdy test się wykonywał - możesz po prostu owinąc każdą pętlę for w drugą, wykonującą się np. 1000 razy).

PS. W tym benchmarku mierzysz głównie szybkość/pojemność pamięci - nie wiem czy to było celem.

PPS. Dla pewności: uruchamiasz na release czy debug? Bo powinineś na tym pierwszym.

edit:

somekind napisał(a)

Jak operujesz na tablicy w pętli for, to stosuj Length, a nie jakąś stałą, bo tak jest wydajniej.

O, nie zauważyłem tego. To też bardzo ważne.

0

Po uwzględnieniu kilku uwag (tym razem w RELEASE mode):

class Program
    {
        static void Main(string[] args)
        {
            var arraySize = 100000;

            var list0 = Generate(arraySize).ToList();
            var list1 = Generate(arraySize).ToList();
            var list2 = Generate(arraySize).ToList();
            var list3 = Generate(arraySize).ToList();


            var watch0 = new Stopwatch();
            var watch1 = new Stopwatch();
            var watch2 = new Stopwatch();
            var watch3 = new Stopwatch();

            watch0.Start();
            for (int j = 0; j < 10; j++)
            {
                for (int i = 0; i < arraySize; i++)
                {
                    list0[i] = list0[i] + "Changed";
                }
            }
            watch0.Stop();
            Console.WriteLine("Time: {0}", watch0.Elapsed.TotalSeconds);


            watch1.Start();
            for (int j = 0; j < 10; j++)
            {
                for (int i = 0; i < arraySize; i++)
                {
                    list1[i] = list1[i] + "Changed";
                }
            }
            watch1.Stop();
            Console.WriteLine("Time: {0}", watch1.Elapsed.TotalSeconds);


            watch2.Start();
            for (int j = 0; j < 10; j++)
            {
                for (var i = 0; i < arraySize; i++)
                {
                    list2[i] = list2[i] + "Changed";
                }
            }
            watch2.Stop();
            Console.WriteLine("Time: {0}", watch2.Elapsed.TotalSeconds);


            watch3.Start();
            for (int j = 0; j < 10; j++)
            {
                for (var i = 0; i < arraySize; i++)
                {
                    list3[i] = list3[i] + "Changed";
                }
            }
            watch3.Stop();
            Console.WriteLine("Time: {0}", watch3.Elapsed.TotalSeconds);
            Console.Read();
        }

       public static IEnumerable<string> Generate(int arraySize)
        {
            for (int i = 0; i < arraySize; i++)
            {
                yield return Guid.NewGuid().ToString();
            }
        }   
    }

Czasy:
0: 0.4987846
1: 0.4988573
2: 0.4493505
3: 0.6331392

Natomiast jeśli zamiast "arraySize" ustawie odpowiednio list.Count, to czasy są następujące:
0: 0.4522383
1: 0.5493413
2: 0.4982633
3: 0.6234763

0
msm napisał(a):

@somekind - indekser listy to tez metoda, a przy benchmarku obejmującym aż 0.01 sekundy to może mieć wpływ na wyniki. I @katelx ma racje, przy benchmarkowaniu zawsze sie pierwsze kilka wywołań pomija

Zgoda, ale jedno wywołanie przy 10000 aż tak dużego wpływu na wynik nie ma. I na pewno nie tłumaczy czemu późniejsze wywołania są szybsze od poprzednich.
Moim zdaniem to kwestia GC.

0

Testowałem niedawno szybkość wielu sposobów liczenia wyrazów ciągu Fibonacciego. Jeden z testów zawierał taki kod:

        long start1 = System.nanoTime();
        for(int i=0;i<howMany;i++)
        {
            fibo1(numbers.get(i));
        }
        long stop1 = System.nanoTime();
        
        wyniki = null;
        long start2 = System.nanoTime();
        for(int i=0;i<howMany;i++)
        {
            fibo2(numbers.get(i));
        }
        long stop2 = System.nanoTime();
        
        wyniki = null;
        zrobione = 1;
        long start3 = System.nanoTime();
        for(int i=0;i<howMany;i++)
        {
            fibo3(numbers.get(i));
        }
        long stop3 = System.nanoTime();

(numbers to kolekcja losowych liczb,fibo1, fibo2, fibo3 to różne funkcje liczące wyraz ciągu)
Czasy wykonania zawsze wyglądają mniej więcej tak (wszystkie próby na tej samej losowej kolekcji):
times.png

1 użytkowników online, w tym zalogowanych: 0, gości: 1