2015-07-10 8 views
6

Niedawno zacząłem uczyć się C# i dowiedziałem się, że istnieją 2 sposoby na uzyskanie kodu wyjściowego w liczbach parzystych, gdy piszę ten kod For loop. Nauczyłem się składni wersji 2, która jest dla mnie intuicyjna. Jednak wersja 1 została przetestowana i znalazłem gdzie indziej w Internecie. Chcę zrozumieć, czy istnieje różnica między tymi dwiema wersjami.Jaka jest bardziej wydajna składnia, aby uzyskać liczbę parzystą w moim projekcie konsoli?

//Version 1 
for (int num = 0; num < 100; num+=2) 
{ 
    Console.WriteLine (num); 
} 

//Version 2 
for (int num = 0; num < 100; num++) 
{ 
    if (num % 2 == 0) 
    { 
     Console.WriteLine (num); 
    } 
} 

Z dwóch możliwych sposobów istnieje różnica między tymi dwiema składniami? Jeśli tak, co jest bardziej skuteczne i dlaczego?

+5

techincally wersja 1 jest szybsze, ale to naprawdę nie ma znaczenia, z prędkością procesorów. – dbarnes

+0

Hmmm Widzę .. Ale obie wersje próbują tego samego, niezależnie od prawidłowości składni? Czy wersja 1 jest szybsza, ponieważ jest to mniejszy kod? @dbarnes – bmeredit

+4

Składnia nie ma znaczenia, że ​​rzeczywisty kod prawdopodobnie nie zostanie zapisany jako il z powodu optymalizacji kompilatora. Co sprawia, że ​​jest szybsza, twoja pętla wykonuje 50 kroków zamiast 100. Jeśli chcesz wiedzieć więcej o tym, kiedy prędkość algorytmu jest odpowiednia, odszukaj notację Big-O. – dbarnes

Odpowiedz

5

Dla danej wartości n (w kodzie przykładowym N = 100)

  • Wersja # 1 ma N/2 dodatki
  • Wersja # 2 nie ma dodatków N, a także N podziałów całkowitych (relatywnie droga operacja).

I zapomniałeś Wersja 3, bit-twiddling. Operacje bitowe są dużo tańsze niż dzielenie, a ponieważ wiemy, że liczby całkowite w świecie C# są dwójkowymi wartościami binarnymi uzupełnienia, stan bitu niskiego rzędu mówi nam, czy liczba całkowita jest parzysta, czy nie, czyli:

bool isEven(int n) { return 0 == (n & 1) ; } 

Możemy napisać wiązkę testową coś takiego:

class Program 
{ 
    public static int Version1(int n) 
    { 
    int cnt = 0; 
    for (int i = 0 ; i < n ; i+=2) 
    { 
     ++cnt; 
    } 
    return cnt; 
    } 

    public static int Version2(int n) 
    { 
    int cnt = 0; 
    for (int i = 0 ; i < n ; ++i) 
    { 
     if (i % 2 == 0) 
     { 
     ++cnt; 
     } 
    } 
    return cnt; 
    } 

    public static int Version3(int n) 
    { 
    int cnt = 0; 
    for (int i = 0 ; i < n ; ++i) 
    { 
     if (0 == (i & 1)) 
     { 
     ++cnt; 
     } 
    } 
    return cnt; 
    } 

    private static void Main(string[] args) 
    { 
    int n = 500000000; 
    Stopwatch timer = new Stopwatch(); 

    timer.Start(); 
    Version1(n); 
    timer.Stop(); 
    Console.WriteLine("{0:c} -- Version #1 (incrementing by 2)" , timer.Elapsed) ; 

    timer.Restart(); 
    Version2(n); 
    timer.Stop(); 
    Console.WriteLine("{0:c} -- Version #2 (incrementing by 1 with modulo test)" , timer.Elapsed) ; 

    timer.Restart(); 
    Version3(n); 
    timer.Stop(); 
    Console.WriteLine("{0:c} -- Version #3 (incrementing by 1 with bit twiddling)" , timer.Elapsed) ; 

    return; 

    } 

} 

I dowiedzieć się, co jest rzeczywiście szybciej. Powyższy program uruchamia pętlę 500 000 000 razy, więc otrzymujemy liczby, które są wystarczająco duże, aby zmierzyć.

Oto czasy dostaję z VS 2013:

  • Debug zbudować (nie zoptymalizowana):

    00:00:00.5500486 -- Version #1 (incrementing by 2) 
    00:00:02.0843776 -- Version #2 (incrementing by 1 with modulo test) 
    00:00:01.2507272 -- Version #3 (incrementing by 1 with bit twiddling) 
    
    • Wersja nr 2 jest 3.789 razy wolniej niż wersja # 1
    • Wersja # 3 2.274 razy wolniej niż wersja # 1
  • Release budowy (zoptymalizowanej)

    00:00:00.1680107 -- Version #1 (incrementing by 2) 
    00:00:00.5109271 -- Version #2 (incrementing by 1 with modulo test) 
    00:00:00.3367961 -- Version #3 (incrementing by 1 with bit twiddling) 
    
    • Wersja # 2 to 3.041 razy wolniej niż w wersji nr 1
    • wersja nr 3 jest 2,005 razy wolniej niż wersja # 1
+0

Dobra odpowiedź ... Nota boczna: Nie jestem pewien, jakie dokładnie słowo miałeś na myśli z "cnt" (wyraźnie brakuje co najmniej jednej samogłoski :)) ... Dodatkowo prawidłowe pomiary czasu powinny przebiegać w fazie rozgrzewki/JIT poza Startem/Stop ... –

+0

@AlexeiLevenkov Nie OP, ale myślałem, że 'cnt' był dość standardowym (choć niejednoznacznym) skurczem" count ". – Sinkingpoint

+0

@Quirliom Próbowałem dodać buźkę ... –

1

Wersja 1 jest bardziej wydajna w tym sensie, że iteruje o połowę mniej razy z tym samym wyjściem. Wersja 1 będzie iterować 50 razy, w przypadku gdy wersja 2 będzie iterować 100 razy, ale wydrukuje tylko połowę wyników. Jednak myślę, że drugi lepiej pokazuje twoje intencje.

+0

Nie zgadzam się. Pierwsza wersja wyraźnie pokazuje intencję, jako przyrost kroku. – StarPilot

2

rzeczywistości mechanizm dodanie (wersja 1) jest nieco lepiej

Efektem dodawania lepiej niż moduł (wartości%) w 0,0070000 milisekund ciągu 2 milionów lub 200.000 iteracje

Ale poważnie ... chyba że masz potrzebę krytycznych mikro optymalizacji, kij z operatorem modułu na łatwe do odczytania kodu raczej niż próba zaoszczędzenia w przybliżeniu czasu 00.0070000 milisekund.

Here Is The Source

3

wersja 1 "szybciej" Z punktu widzenia analizy czystego (niezależnie od optymalizacji kompilacji). Ale jak dbarnes wspomniany w komentarzach do twojego pytania, to, co kodujesz w C# i co wychodzi po drugiej stronie kompilatora może być zupełnie inne. Wersja 2 jest "wolniejsza", ponieważ ma więcej iteracji pętli do przełączania i ma nieco większą złożoność kodu z tym if-instrukcją.

nie dotyczą sobie zbyt wiele z optymalizacją na początku (to się nazywa „przedwczesna optymalizacja”, aby zajmować się wykonanie przed wiesz, że istnieje i może udowodnić że jest to problem wart rozwiązywania). Po prostu kodu, aby dowiedzieć się, a następnie refactor, a następnie zajrzyj do kwestii wydajności, jeśli istnieją.

W rzeczywistości, spekulowałbym, że kompilator może być skonfigurowany do "rozwinięcia" obu tych pętli: pozbywa się pętli i po prostu duplikuje ciało pętli 50 lub 100 razy dla wersji 1 lub 2 odpowiednio.

3

Można dokładnie zmierzyć, która metoda jest szybsza, z dokładnością do 10000. milisekundy, znaną również jako "Tick" w strukturze .Net.

Odbywa się to za pomocą stopera klasy w przestrzeni nazw System.Diagnostic:

var sw1 = new System.Diagnostics.Stopwatch(); 
var sw2 = new System.Diagnostics.Stopwatch(); 

//Version 1 
sw1.Start(); 
for (int num = 0; num < 100; num += 2) 
{ 
    Console.WriteLine(num); 
} 
sw1.Stop(); 

//Version 2 
sw2.Start(); 
for (int num = 0; num < 100; num++) 
{ 
    if (num % 2 == 0) 
    { 
     Console.WriteLine(num); 
    } 
} 
sw2.Stop(); 

Console.Clear(); 
Console.WriteLine("Ticks for first method: " + sw1.ElapsedTicks); 
Console.WriteLine("Ticks for second method: " + sw2.ElapsedTicks); 

Wyjście pokaże, że pierwsza metoda jest szybsza.

Dlaczego tak się dzieje? W pierwszej wersji, ignorując wyjście konsoli, wykonano tylko jedną operację (+= 2), a na koniec program przechodzi przez 50 cykli pętli.

W drugiej wersji musi wykonać dwie operacje (++ i % 2) oraz jedno dodatkowe porównanie (num % 2 == 0) oprócz 100 cykli w pętli.

Jeśli mierzysz program dokładnie w ten sposób, będzie to miało dużą różnicę w czasie, jak wiele milisekund. Wynika to z faktu, że Console.WriteLine zajmuje naprawdę dużo czasu. Ponieważ w drugiej wersji robi się 50 razy więcej, zajmuje to znacznie więcej czasu. Jeśli chcesz zmierzyć sam algorytm, pomiń jego wyjście.

Jeśli to zrobisz, różnica w kleszczach na moim komputerze wynosi średnio 24 ticków do 43 ticków.

Podsumowując, pierwsza metoda jest bardziej wydajna, o około 19/10000 milisekund.

+3

Oczywiście, jeśli pominiesz wyjście, _i będziesz miał optymalizacje on_, żadna pętla nie osiągnie niczego, więc może mieć czas zerowy. –

+7

"ponieważ jest to [' WriteLine'] zrobione 50 razy więcej w drugiej wersji "- nie widzę, jak to może być prawdą ... –

1

Wersja 1 jest bardziej wydajna. Ponieważ "wersja 2" ma jedno porównanie więcej niż "wersja 1", podczas gdy "wersja 1" wymaga dwóch porównań.

//Version 1 
for (int num = 0; num < 100; num+=2)  // Two comparisons 
{ 
    Console.WriteLine (num); 
} 

//Version 2 
for (int num = 0; num < 100; num++)   // Two comparisons 
{ 
    if (num % 2 == 0)      // One comparison 
    { 
     Console.WriteLine (num); 
    } 
} 
0

Ten sposób jest szybszy niż metoda 1 i metoda 2, ponieważ rekursja jest szybsza niż pętla.

 var sw3 = new System.Diagnostics.Stopwatch(); 
     Action<int, int> printEvens = null; 
     printEvens = (index, count) => 
     { 
      if (index % 2 == 0) 
       Console.WriteLine(index.ToString()); 
      if (index < count) 
       printEvens(++index, count); 
     }; 

     sw3.Start(); 
     printEvens(0, 100); 
     sw3.Stop(); 

Wyjście na moim komputerze jest ..

Metoda 1: 96.282 Metoda 2: 184336 Metoda 3: Recursion 59188

+0

To jest bardziej wydajne, ale nie o to, co było zadawane. –

+1

Ogólnie rekursja jest * nie * szybsza niż pętla w C#. –

+0

Jeśli wyłączysz metodę 1 i 2 znacznika czasu konsolety console.writeline i QUICK, jak 8 ticków. W tym scenariuszu metoda 3 to crap, 459. Ale po dodaniu logiki GUI, metoda 3 jest szybsza. Nie pewny dlaczego. Lub jeśli zwiększysz liczbę iteracji, metoda 3 będzie wolniejsza i wolniejsza. Mogę sobie więc wyobrazić, że gdy ślad stosu jest większy, to rekurencja jest wolniejsza. Im głębsza rekurencja, tym wolniej. –

Powiązane problemy