Wszędzie czytam, że operator trójskładnikowy ma być szybszy lub przynajmniej taki sam jak jego odpowiednik if
- else
blok.
Jednak wykonałem następujący test i okazało się, że tak nie jest:
Random r = new Random();
int[] array = new int[20000000];
for(int i = 0; i < array.Length; i++)
{
array[i] = r.Next(int.MinValue, int.MaxValue);
}
Array.Sort(array);
long value = 0;
DateTime begin = DateTime.UtcNow;
foreach (int i in array)
{
if (i > 0)
{
value += 2;
}
else
{
value += 3;
}
// if-else block above takes on average 85 ms
// OR I can use a ternary operator:
// value += i > 0 ? 2 : 3; // takes 157 ms
}
DateTime end = DateTime.UtcNow;
MessageBox.Show("Measured time: " + (end-begin).TotalMilliseconds + " ms.\r\nResult = " + value.ToString());
Mój komputer potrzebował 85 ms na uruchomienie powyższego kodu. Ale jeśli skomentuję fragment if
- else
i odkomentuję trójskładnikową linię operatora, zajmie to około 157 ms.
Dlaczego to się dzieje?
c#
performance
conditional-operator
użytkownik1032613
źródło
źródło
DateTime
do pomiaru wydajności. ZastosowanieStopwatch
. Następnie czas raczej dłuższy - to bardzo krótki czas na zmierzenie.Random
obiektu, aby zawsze miał tę samą sekwencję. Jeśli testujesz inny kod z różnymi danymi, możesz bardzo dobrze zobaczyć różnice w wydajności.Odpowiedzi:
Aby odpowiedzieć na to pytanie, zbadamy kod asemblera wygenerowany przez JIT X86 i X64 dla każdego z tych przypadków.
X86, jeśli / to
X86, trójskładnikowy
X64, jeśli / to
X64, trójskładnikowy
Po pierwsze: dlaczego kod X86 jest o wiele wolniejszy niż X64?
Wynika to z następujących cech kodu:
i
z tablicy, podczas gdy JIT X86 umieszcza w pętli kilka operacji na stosie (dostęp do pamięci).value
jest 64-bitową liczbą całkowitą, która wymaga 2 instrukcji maszynowych na X86 (add
po których następujeadc
), ale tylko 1 instrukcji na X64 (add
).Po drugie: dlaczego operator trójskładnikowy działa wolniej zarówno na X86, jak i X64?
Wynika to z subtelnej różnicy w kolejności operacji wpływającej na optymalizator JIT. Aby JIT operatora trójskładnikowego, zamiast bezpośrednio kodować
2
i3
wadd
samych instrukcjach maszyny, JIT tworzy zmienną pośrednią (w rejestrze) do przechowywania wyniku. Rejestr ten jest następnie rozszerzany z 32-bitowych na 64-bitowe przed dodaniem govalue
. Ponieważ wszystko to odbywa się w rejestrach X64, pomimo znacznego wzrostu złożoności dla operatora trójskładnikowego, wpływ netto jest nieco zminimalizowany.Z drugiej strony wpływ na JIT X86 ma większy wpływ, ponieważ dodanie nowej wartości pośredniej w pętli wewnętrznej powoduje „rozlanie” innej wartości, co skutkuje co najmniej 2 dodatkowymi dostępami do pamięci w pętli wewnętrznej (patrz: dostępy do
[ebp-14h]
w kodzie trójskładnikowym X86).źródło
EDYCJA: Wszystkie zmiany ... patrz poniżej.
I nie można odtworzyć swoje wyniki na CLR x64, ale może na x86. Na x64 widzę małą różnicę (mniej niż 10%) między operatorem warunkowym a if / else, ale jest znacznie mniejsza niż widać.
Wprowadziłem następujące potencjalne zmiany:
/o+ /debug-
i uruchamiaj poza debuggeremStopwatch
Wyniki z
/platform:x64
(bez wierszy „ignoruj”):Wyniki z
/platform:x86
(bez wierszy „ignoruj”):Szczegóły mojego systemu:
Tak więc w przeciwieństwie do wcześniej, myślę, że są widząc różnicę - a to wszystko zrobić z JIT x86. Nie chciałbym powiedzieć dokładnie, co powoduje różnicę - mogę później zaktualizować post, podając więcej szczegółów, jeśli będę miał problem z wejściem na cordbg :)
Co ciekawe, bez uprzedniego posortowania tablicy, kończę na testach, które trwają około 4,5 razy dłużej, przynajmniej na x64. Domyślam się, że ma to związek z prognozowaniem gałęzi.
Kod:
źródło
Różnica tak naprawdę nie ma wiele wspólnego z if / else vs trójka.
Patrząc na rozczłonkowane dezasemblacje (nie będę tu ponownie wklejać, proszę zobaczyć odpowiedź @ 280Z28), okazuje się, że porównujesz jabłka i pomarańcze . W jednym przypadku tworzysz dwie różne
+=
operacje ze stałymi wartościami, a ta, którą wybierasz, zależy od warunku, aw drugim przypadku tworzysz miejsce, w+=
którym wartość do dodania zależy od warunku.Jeśli chcesz naprawdę porównać, jeśli / w przeciwieństwie do trójki, byłoby to bardziej sprawiedliwe porównanie (teraz oba będą równie „wolne”, lub możemy nawet powiedzieć, że trójka jest nieco szybsza):
vs.
Teraz demontaż dla
if/else
staje się, jak pokazano poniżej. Zauważ, że jest to nieco gorsze niż przypadek trójskładnikowy, ponieważ zakończył również używanie rejestrów dla zmiennej loop (i
).źródło
diff
, ale trójskładnikowy jest nadal dużo wolniejszy - wcale nie tak, jak powiedziałeś. Czy zrobiłeś eksperyment przed opublikowaniem tej „odpowiedzi”?Edytować:
Dodano przykład, który można wykonać za pomocą instrukcji if-else, ale nie operatora warunkowego.
Przed odpowiedzią spójrz na [ Który jest szybszy? ] na blogu pana Lipperta. I myślę, że odpowiedź pana Ersönmeza jest tutaj najbardziej poprawna.
Próbuję wspomnieć o czymś, o czym powinniśmy pamiętać w języku programowania wysokiego poziomu.
Po pierwsze, nigdy nie słyszałem, że operator warunkowy powinien być szybszy lub równie wydajny z instrukcją if-else w C♯ .
Powód jest prosty, co jeśli nie ma operacji z instrukcją if-else:
Wymaganie operatora warunkowego jest takie, że po każdej ze stron musi być wartość , aw C♯ wymaga również, aby obie strony miały
:
ten sam typ. To po prostu odróżnia ją od instrukcji if-else. W ten sposób twoje pytanie staje się pytaniem, w jaki sposób generowana jest instrukcja kodu maszynowego, aby różnicę w wydajności.W przypadku operatora warunkowego semantycznie jest to:
Niezależnie od tego, jakie wyrażenie zostanie ocenione, istnieje wartość.
Ale z instrukcją if-else:
Jeśli wyrażenie zostanie ocenione jako prawdziwe, zrób coś; jeśli nie, zrób inną rzecz.
Wartość niekoniecznie jest związana z instrukcją if-else. Twoje założenie jest możliwe tylko przy optymalizacji.
Kolejny przykład pokazujący różnicę między nimi byłby następujący:
powyższy kod się kompiluje, jednak zamień instrukcję if-else operatorem warunkowym po prostu nie skompiluje:
Operator warunkowy i instrukcje if-else są pojęciowe tak samo, gdy robisz to samo, może nawet szybciej z operatorem warunkowym w C , ponieważ C jest bliżej zestawu platformy.
W podanym przez ciebie oryginalnym kodzie operator warunkowy jest używany w pętli foreach, która zepsułaby rzeczy, aby zobaczyć różnicę między nimi. Więc proponuję następujący kod:
a poniżej znajdują się dwie wersje IL zoptymalizowanej i nie. Ponieważ są one długie, używam do wyświetlenia obrazu, prawa strona jest zoptymalizowana:
W obu wersjach kodu IL operatora warunkowego wygląda na krótszą niż instrukcja if-else i nadal istnieją wątpliwości co do ostatecznie wygenerowanego kodu maszynowego. Poniżej przedstawiono instrukcje dotyczące obu metod, a pierwszy obraz jest niezoptymalizowany, a drugi jest zoptymalizowany:
Niezoptymalizowane instrukcje: (Kliknij, aby zobaczyć obraz w pełnym rozmiarze).
Zoptymalizowane instrukcje: (Kliknij, aby zobaczyć obraz w pełnym rozmiarze).
W tym ostatnim żółty blok jest kodem wykonywanym tylko wtedy
i<=0
, a niebieski blok jest kiedyi>0
. W obu wersjach instrukcji instrukcja if-else jest krótsza.Należy pamiętać, że dla różnych instrukcji [ CPI ] niekoniecznie jest taki sam. Logicznie, dla identycznej instrukcji, więcej instrukcji kosztuje dłuższy cykl. Jeśli jednak uwzględniony zostanie również czas pobierania instrukcji oraz pamięć podręczna / pamięć podręczna, rzeczywisty całkowity czas wykonania zależy od procesora. Procesor może również przewidywać gałęzie.
Współczesne procesory mają jeszcze więcej rdzeni, dzięki temu wszystko może być bardziej złożone. Jeśli jesteś użytkownikiem procesora Intel, możesz zajrzeć do [ Intel® 64 i IA-32 Architectures Optimization Reference Manual ].
Nie wiem, czy istniała CLR zaimplementowana sprzętowo, ale jeśli tak, prawdopodobnie przyspieszysz dzięki operatorowi warunkowemu, ponieważ IL jest oczywiście mniejsza.
Uwaga: Wszystkie kody maszynowe mają format x86.
źródło
Zrobiłem to, co zrobił Jon Skeet, przejrzałem 1 iterację i 1000 iteracji i uzyskałem inny wynik niż OP i Jon. W moim przypadku trójskładnik jest tylko nieco szybszy. Poniżej znajduje się dokładny kod:
Dane wyjściowe z mojego programu:
Kolejny przebieg w milisekundach:
Działa w 64-bitowym systemie XP i działałem bez debugowania.
Edycja - działa w x86:
Za pomocą x86 jest duża różnica. Dokonano tego bez debugowania na tym samym 64-bitowym komputerze xp jak poprzednio, ale zbudowany dla procesorów x86. To bardziej przypomina OP.
źródło
Wygenerowany kod asemblera opowie historię:
Generuje:
Natomiast:
Generuje:
Tak więc trójskładnik może być krótszy i szybszy po prostu dzięki użyciu mniejszej liczby instrukcji i braku skoków, jeśli szukasz wartości prawda / fałsz. Jeśli użyjesz wartości innych niż 1 i 0, otrzymasz taki sam kod jak if / else, na przykład:
Generuje:
Który jest taki sam jak if / else.
źródło
Uruchom bez debugowania ctrl + F5. Wygląda na to, że debugger znacznie spowalnia zarówno ifs, jak i trójskładnikowy, ale wydaje się, że znacznie bardziej spowalnia operatora trójskładnikowego.
Po uruchomieniu następującego kodu tutaj są moje wyniki. Myślę, że mała różnica milisekund jest spowodowana przez kompilator optymalizujący max = max i usuwający go, ale prawdopodobnie nie dokonuje takiej optymalizacji dla operatora trójskładnikowego. Gdyby ktoś mógł sprawdzić zespół i potwierdzić to byłoby niesamowite.
Kod
źródło
Patrząc na wygenerowaną IL, jest w niej 16 operacji mniej niż w instrukcji if / else (kopiowanie i wklejanie kodu @ JonSkeet). Nie oznacza to jednak, że proces ten powinien być szybszy!
Podsumowując różnice w IL, metoda if / else tłumaczy prawie tak samo, jak odczytuje kod C # (wykonując dodawanie w gałęzi), podczas gdy kod warunkowy ładuje 2 lub 3 na stos (w zależności od wartości) i następnie dodaje ją do wartości spoza warunku.
Inną różnicą jest zastosowana instrukcja rozgałęzienia. Metoda if / else używa polecenia brtrue (rozgałęzienie, jeśli prawda), aby przeskoczyć nad pierwszym warunkiem, oraz bezwarunkowego rozgałęzienia, aby przeskoczyć z pierwszej instrukcji if. Kod warunkowy używa bgt (gałąź, jeśli jest większa niż) zamiast brtrue, co może być wolniejszym porównaniem.
Ponadto (po przeczytaniu o prognozowaniu gałęzi) może istnieć kara wydajnościowa za mniejszą gałąź. Gałąź warunkowa ma tylko 1 instrukcję w gałęzi, ale if / else ma 7. To także wyjaśnia, dlaczego istnieje różnica między używaniem long i int, ponieważ zmiana na int zmniejsza liczbę instrukcji w gałęziach if / else o 1 (co zmniejsza wyprzedzenie odczytu)
źródło
W poniższym kodzie, jeśli / else wydaje się być około 1,4 razy szybszy niż operator trójskładnikowy. Odkryłem jednak, że wprowadzenie zmiennej tymczasowej skraca czas działania operatora trójskładnikowego około 1,4 razy:
źródło
Zbyt wiele świetnych odpowiedzi, ale znalazłem coś interesującego, bardzo proste zmiany mają wpływ. Po wykonaniu poniższej zmiany, aby wykonać if-else i operator trójskładnikowy, zajmie to ten sam czas.
zamiast pisać poniżej linii
Użyłem tego
Jedna z poniższych odpowiedzi wspomina również, że to zły sposób na napisanie operatora trójskładnikowego.
Mam nadzieję, że pomoże ci to napisać trójskładnikowego operatora, zamiast zastanawiać się, który z nich jest lepszy.
Zagnieżdżony operator trójskładnikowy: Znalazłem zagnieżdżony operator trójskładnikowy i wielu, jeśli w przeciwnym razie wykonanie bloku zajmie ten sam czas.
źródło