Czy kompilator C ++ usuwa / optymalizuje bezużyteczne nawiasy?

19

Czy kod

int a = ((1 + 2) + 3); // Easy to read

działa wolniej niż

int a = 1 + 2 + 3; // (Barely) Not quite so easy to read

lub są nowoczesnymi kompilatorami wystarczająco sprytnymi, aby usunąć / zoptymalizować „bezużyteczne” nawiasy.

Może to wydawać się bardzo drobnym problemem związanym z optymalizacją, ale wybór C ++ zamiast C # / Java / ... polega na optymalizacji (IMHO).

Serge
źródło
9
Myślę, że C # i Java też to zoptymalizują. Wierzę, że kiedy parsują i tworzą AST, po prostu usuwają oczywiste bezużyteczne rzeczy.
Farid Nouri Neshat
5
Wszystko, co przeczytałem, wskazuje na kompilację JIT, która z łatwością zapewnia kompilację z wyprzedzeniem, a więc sama w sobie nie jest zbyt przekonującym argumentem. Przywołujesz programowanie gier - prawdziwym powodem faworyzowania kompilacji z wyprzedzeniem jest to, że jest przewidywalna - dzięki kompilacji JIT nigdy nie wiadomo, kiedy kompilator się uruchomi i spróbuje rozpocząć kompilację kodu. Chciałbym jednak zauważyć, że kompilacja z wyprzedzeniem do kodu natywnego nie wyklucza się wzajemnie z odśmiecaniem, patrz np. Standard ML i D. I widziałem przekonujące argumenty, że odśmiecanie jest bardziej wydajne ...
Doval
7
... niż RAII i inteligentne wskaźniki, więc chodzi raczej o podążanie dobrze pokonaną ścieżką (C ++) w porównaniu ze stosunkowo nieprzeczytaną ścieżką programowania gier w tych językach. Chciałbym również zauważyć, że martwienie się nawiasami jest szalone - widzę, skąd pochodzisz, ale to jest absurdalna mikrooptymalizacja. Wybór struktur danych i algorytmów w twoim programie z pewnością zdominuje wydajność, a nie takie trywialności.
Doval
6
Um ... Jakiej konkretnie optymalizacji oczekujesz? Jeśli mówisz o analizie statycznej, w większości znanych mi języków zostanie ona zastąpiona statystycznie znanym wynikiem (implementacje oparte na LLVM nawet to egzekwują, AFAIK). Jeśli mówisz o kolejności wykonania, nie ma to znaczenia, ponieważ jest to ta sama operacja i bez skutków ubocznych. Dodanie i tak wymaga dwóch operandów. A jeśli używasz tego do porównywania C ++, Java i C # pod względem wydajności, brzmi to tak, jakbyś nie miał jasnego pojęcia o tym, czym są optymalizacje i jak one działają, więc zamiast tego powinieneś skupić się na nauce.
Theodoros Chatzigiannakis
5
Zastanawiam się, dlaczego a) uważasz nawiasowe wyrażenie za bardziej czytelne (dla mnie wygląda to po prostu brzydko, wprowadzające w błąd (dlaczego akcentują ten konkretny porządek? Czy nie powinno to być tutaj pomocnicze?) I niezgrabne) b) dlaczego nie pomyślałbyś bez w nawiasach może to działać lepiej (wyraźne parsowanie parenów jest łatwiejsze dla maszyny niż konieczność uzasadnienia poprawkami operatora. Jak jednak mówi Marc van Leuwen, nie ma to absolutnie żadnego wpływu na czas działania).
lewo około

Odpowiedzi:

87

Kompilator tak naprawdę nigdy nie wstawia ani nie usuwa nawiasów; po prostu tworzy drzewo parsowania (w którym nie ma nawiasów) odpowiadające Twojemu wyrażeniu, a robiąc to, musi przestrzegać nawiasów, które napisałeś. Jeśli w pełni nawiasujesz wyraz, to od razu czytelnik zrozumie, czym jest to drzewo parsowania; jeśli dojdziesz do skrajności polegającej na wstawianiu rażąco redundantnych nawiasów, int a = (((0)));wówczas wprowadzisz niepotrzebny nacisk na neurony czytnika, jednocześnie marnując niektóre cykle w parserze, nie zmieniając jednak wynikowego drzewa analizy (a zatem wygenerowanego kodu) ) w najmniejszym stopniu.

Jeśli nie napiszesz żadnych nawiasów, analizator składni musi nadal wykonać swoje zadanie, tworząc drzewo analizy składniowej, a reguły dotyczące pierwszeństwa operatora i asocjatywności mówią dokładnie, jakie drzewo analizy musi zbudować. Możesz uznać te reguły za wskazujące kompilatorowi, które (niejawne) nawiasy należy wstawić do kodu, chociaż parser tak naprawdę nigdy nie radzi sobie z nawiasami w tym przypadku: został on po prostu skonstruowany tak, aby tworzył to samo drzewo analizy jak nawiasy były obecne w niektórych miejscach. Jeśli umieścisz nawiasy w dokładnie tych miejscach, jak w int a = (1+2)+3;(asocjatywność +jest po lewej), parser dojdzie do tego samego wyniku nieco inną drogą. Jeśli wstawisz inne nawiasy jak wint a = 1+(2+3);następnie wymuszasz inne drzewo analizy, co prawdopodobnie spowoduje wygenerowanie innego kodu (choć może nie, ponieważ kompilator może zastosować transformacje po zbudowaniu drzewa analizy, o ile efekt wykonania wynikowego kodu nigdy nie będzie różny dla to). Zakładając, że istnieje różnica w kodzie resuting, ogólnie nie można powiedzieć, co jest bardziej wydajne; najważniejsze jest oczywiście to, że przez większość czasu parsowania drzewa nie dają matematycznie równoważnych wyrażeń, więc porównanie ich szybkości wykonania jest poza tym: należy po prostu napisać wyrażenie, które daje właściwy wynik.

Wynik jest następujący: użyj nawiasów, jeśli jest to konieczne dla poprawności i dla pożądanej czytelności; jeśli są zbędne, nie mają żadnego wpływu na szybkość wykonywania (i mają znikomy wpływ na czas kompilacji).

I nic z tego nie ma nic wspólnego z optymalizacją , która pojawia się po zbudowaniu parsowanego drzewa, więc nie może wiedzieć, jak zostało zbudowane parsowanie. Odnosi się to bez zmiany od najstarszych i najgłupszych kompilatorów na najmądrzejsze i najnowocześniejsze. Tylko w języku interpretowanym (gdzie „czas kompilacji” i „czas wykonania” są zbieżne) może istnieć kara za zbędne nawiasy, ale nawet wtedy myślę, że większość takich języków jest zorganizowana tak, że przynajmniej faza parsowania jest wykonywana tylko raz dla każdej instrukcji (przechowywanie jej wstępnie przetworzonej formy do wykonania).

Marc van Leeuwen
źródło
s / oldes / najstarszy /. Dobra odpowiedź, +1.
David Conrad
23
Ostrzeżenietotalne nitpick: „Pytanie nie jest zbyt dobrze postawione”. Zasadniczo pytanie brzmi: „optymalizacja dla X, wybierz A lub B i dlaczego? Co dzieje się pod spodem?”, Co przynajmniej dla mnie bardzo wyraźnie sugeruje, czym jest luka w wiedzy. Wada w pytaniu, na które słusznie wskazujesz i do którego bardzo dobrze się odnosisz, polega na tym, że opiera się ono na wadliwym modelu mentalnym.
Jonas Kölker
W przypadku a = b + c * d;, a = b + (c * d);będzie [nieszkodliwe] zbędne nawiasach. Jeśli pomogą ci uczynić kod bardziej czytelnym, dobrze. a = (b + c) * d;byłyby niepotrzebnymi nawiasami - w rzeczywistości zmieniają powstałe drzewo parsowania i dają inny wynik. Jest to całkowicie legalne do zrobienia (w rzeczywistości konieczne), ale nie są tym samym co domyślna grupa domyślna.
Phil Perry
1
@OrangeDog to prawda, szkoda, że ​​komentarz Bena wydał kilka osób, które lubią twierdzić, że maszyny wirtualne są szybsze niż natywne.
gbjbaanb
1
@ JonasKölker: Moje pierwsze zdanie faktycznie odnosi się do pytania sformułowanego w tytule: tak naprawdę nie można odpowiedzieć na pytanie, czy kompilator wstawia lub usuwa nawiasy, ponieważ jest to oparte na błędnym wyobrażeniu o tym, jak działają kompilatory. Zgadzam się jednak, że jest całkiem jasne, którą lukę wiedzy należy usunąć.
Marc van Leeuwen
46

Nawiasy są wyłącznie dla twojej korzyści - nie kompilatory. Kompilator utworzy prawidłowy kod maszynowy, który będzie reprezentował Twoją instrukcję.

FYI, kompilator jest wystarczająco sprytny, aby zoptymalizować go całkowicie, jeśli to możliwe. W twoich przykładach zmieniłoby się to int a = 6;w czasie kompilacji.

gbjbaanb
źródło
9
Oczywiście - włóż tyle nawiasów, ile chcesz i pozwól kompilatorowi ciężko pracować nad
znalezieniem
23
@ Prawdziwe programowanie @Sgege polega bardziej na czytelności kodu niż na wydajności. Nienawidzisz siebie w przyszłym roku, kiedy będziesz musiał debugować awarię, a masz tylko „zoptymalizowany” kod do przejścia.
maniak zapadkowy
1
@ratchetfreak, masz rację, ale wiem też, jak skomentować mój kod. int a = 6; // = (1 + 2) + 3
Serge
20
@ Serge Wiem, że nie wytrzyma po roku poprawek, z czasem komentarze i kod przestaną być zsynchronizowane, a potem skończysz zint a = 8;// = 2*3 + 5
ratchet maniakiem
21
lub z www.thedailywtf.com:int five = 7; //HR made us change this to six...
Kaczka Mooing
23

Odpowiedź na pytanie, które faktycznie zadałeś, brzmi „nie”, ale odpowiedź na pytanie, które chciałeś zadać, brzmi „tak”. Dodanie nawiasów nie spowalnia kodu.

Zadałeś pytanie dotyczące optymalizacji, ale nawiasy nie mają nic wspólnego z optymalizacją. Kompilator stosuje różne techniki optymalizacji z zamiarem poprawienia rozmiaru lub szybkości generowanego kodu (czasami oba). Na przykład może przyjąć wyrażenie A ^ 2 (A do kwadratu) i zastąpić je przez A x A (A pomnożone przez siebie), jeśli jest to szybsze. Odpowiedź brzmi: nie, kompilator nie robi nic innego w fazie optymalizacji w zależności od tego, czy nie ma nawiasów.

Myślę, że chciałeś zapytać, czy kompilator nadal generuje ten sam kod, jeśli dodasz niepotrzebne nawiasy do wyrażenia, w miejscach, które Twoim zdaniem mogą poprawić czytelność. Innymi słowy, jeśli dodasz nawiasy, kompilator jest wystarczająco sprytny, aby je usunąć, zamiast generować gorszy kod. Odpowiedź brzmi: tak, zawsze.

Pozwól, że powiem to ostrożnie. Jeśli dodasz nawiasy do wyrażenia, które są absolutnie niepotrzebne (nie mają żadnego wpływu na znaczenie lub kolejność oceny wyrażenia), kompilator po cichu je odrzuci i wygeneruje ten sam kod.

Istnieją jednak pewne wyrażenia, w których pozornie niepotrzebne nawiasy faktycznie zmienią kolejność obliczania wyrażenia, w takim przypadku kompilator wygeneruje kod, aby wprowadzić w życie to, co napisałeś, co może być inne niż zamierzałeś. Oto przykład. Nie rób tego!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

Dodaj nawiasy, jeśli chcesz, ale upewnij się, że naprawdę są niepotrzebne!

Nigdy nie robię. Istnieje ryzyko błędu bez rzeczywistych korzyści.


Przypis: zachowanie bez znaku ma miejsce, gdy podpisane wyrażenie liczb całkowitych zwraca wartość, która jest poza zakresem, który może wyrazić, w tym przypadku od -32767 do +32767. To skomplikowany temat, poza zakresem tej odpowiedzi.

david.pfx
źródło
Niezdefiniowane zachowanie w ostatnim wierszu jest spowodowane tym, że podpisany skrót ma tylko 15 bitów po znaku, więc maksymalny rozmiar 32767, prawda? W tym trywialnym przykładzie kompilator powinien ostrzec o przepełnieniu, prawda? +1 za licznik w każdym przypadku. Gdyby były parametrami funkcji, nie pojawiłoby się ostrzeżenie. Ponadto, jeśli anaprawdę można niepodpisać, prowadzenie obliczeń z -a + bmoże łatwo przepełnić, jeśli abyłyby negatywne i bpozytywne.
Patrick M,
@PatrickM: patrz edycja. Niezdefiniowane zachowanie oznacza, że ​​kompilator może robić, co chce, w tym wydawać ostrzeżenie lub nie. Niepodpisana arytmetyka nie wytwarza UB, ale zmniejsza modulo następną wyższą potęgę dwóch.
david.pfx
Wyrażenie (b+c)w ostatnim wierszu będzie promować swoje argumenty int, więc jeśli kompilator nie zdefiniuje int16 bitów (albo dlatego, że jest starożytny, albo wymierza mały mikrokontroler), ostatni wiersz byłby całkowicie uzasadniony.
supercat
@supercat: Nie wydaje mi się. Typowym typem i rodzajem wyniku powinno być krótkie int. Jeśli nie jest to coś, o co kiedykolwiek pytano, być może chciałbyś zadać pytanie?
david.pfx
@ david.pfx: Zasady promocji arytmetycznych C są dość jasne: wszystko mniejsze niż intzostanie awansowane, intchyba że ten typ nie byłby w stanie przedstawić wszystkich swoich wartości, w którym to przypadku zostałby awansowany unsigned int. Kompilatory mogą pominąć promocje, jeśli wszystkie zdefiniowane zachowania byłyby takie same, jak gdyby promocje zostały uwzględnione . Na komputerze, na którym typy 16-bitowe zachowują się jak zawijany abstrakcyjny pierścień algebraiczny, (a + b) + ci a + (b + c) będą równoważne. Gdyby intjednak typ 16-bitowy był uwięziony w wyniku przepełnienia, byłyby przypadki, w których jedno z wyrażeń ...
supercat
7

Wsporniki służą wyłącznie do zmiany kolejności pierwszeństwa operatora. Po skompilowaniu nawiasy już nie istnieją, ponieważ środowisko wykonawcze ich nie potrzebuje. Proces kompilacji usuwa wszystkie nawiasy, spacje i inny cukier syntaktyczny, którego potrzebujemy ty i ja, i zamienia wszystkich operatorów w coś [znacznie] prostszego do wykonania przez komputer.

Więc, gdzie ty i ja możemy zobaczyć ...

  • „int a = ((1 + 2) + 3);”

... kompilator może emitować coś takiego:

  • Char [1] :: „a”
  • Int32 :: DeclareStackVariable ()
  • Int32 :: 0x00000001
  • Int32 :: 0x00000002
  • Int32 :: Add ()
  • Int32 :: 0x00000003
  • Int32 :: Add ()
  • Int32 :: AssignToVariable ()
  • void :: DiscardResult ()

Program jest uruchamiany od początku i kolejno wykonując każdą instrukcję.
Pierwszeństwo operatorów ma teraz „kto pierwszy, ten lepszy”.
Wszystko jest silnie wpisany, ponieważ kompilator działało wszystko , że się póki to łzawienie oryginalną składnię siebie.

OK, to nic takiego jak te, z którymi ty i ja mamy do czynienia, ale wtedy tego nie prowadzimy!

Phill W.
źródło
4
Nie ma jednego kompilatora C ++, który wyprodukowałby coś takiego nawet zdalnie. Generalnie generują rzeczywisty kod procesora, a asembler też tego nie wygląda.
MSalters
3
celem było wykazanie różnicy w strukturze między kodem w postaci napisanej a skompilowanym wyjściem. nawet tutaj większość ludzi nie byłaby w stanie odczytać rzeczywistego kodu maszynowego lub zestawu
DHall
@MSalters Nitpicking clang wyemituje „coś takiego”, jeśli traktujesz LLVM ISA jako „coś takiego” (to SSA nie jest oparte na stosie). Biorąc pod uwagę, że można napisać backend JVM dla LLVM, a JVM ISA (AFAIK) jest oparty na stosie clang-> llvm-> JVM wyglądałby bardzo podobnie.
Maciej Piechotka
Nie sądzę, że LLVM ISA ma możliwość definiowania zmiennych stosu nazw przy użyciu literałów łańcucha wykonawczego (tylko dwie pierwsze instrukcje). To poważnie miesza czas działania i czas kompilacji. To rozróżnienie ma znaczenie, ponieważ to pytanie dotyczy właśnie tego zamieszania.
MSalters
6

Zależy, czy jest zmiennoprzecinkowy, czy nie:

  • W liczbach zmiennoprzecinkowych dodawanie arytmetyczne nie jest skojarzone, więc optymalizator nie może zmienić kolejności operacji (chyba że dodasz przełącznik kompilatora Fastmath).

  • W operacjach na liczbach całkowitych można je zmienić.

W twoim przykładzie oba będą działać dokładnie w tym samym czasie, ponieważ skompilują się do dokładnie tego samego kodu (dodawanie jest oceniane od lewej do prawej).

jednak nawet Java i C # będą w stanie go zoptymalizować, po prostu zrobią to w czasie wykonywania.

maniak zapadkowy
źródło
+1 za wywołanie operacji zmiennoprzecinkowych nie są asocjacyjne.
Doval
W przykładzie pytania nawiasy nie zmieniają domyślnego (lewego) skojarzenia, więc ten punkt jest dyskusyjny.
Marc van Leeuwen,
1
Co do ostatniego zdania, nie sądzę. Zarówno w Javie A, jak i C # kompilator wygeneruje zoptymalizowany kod bajtowy / IL. Nie ma to wpływu na środowisko wykonawcze.
Stefano Altieri
IL nie działa na tego rodzaju wyrażenia, instrukcje pobierają pewną liczbę wartości ze stosu i zwracają pewną liczbę wartości (zwykle 0 lub 1) do stosu. Mówienie o tego rodzaju optymalizacji w środowisku wykonawczym w języku C # jest nonsensem.
Jon Hanna
6

Typowy kompilator C ++ tłumaczy na kod maszynowy, a nie sam C ++ . Usuwa bezużyteczne pareny, tak, ponieważ zanim to się skończy, nie ma żadnych parens. Kod maszynowy nie działa w ten sposób.

The Spooniest
źródło
5

Oba kody kończą na sztywno jako 6:

movl    $6, -4(%rbp)

Sprawdź tutaj

MonoThreaded
źródło
2
Czym jest ten assembly.ynh.io ?
Peter Mortensen
To pseudo-kompilator, który przekształca kod C w asemblerze x86 online
MonoThreaded
1

Nie, ale tak, ale może, ale może na odwrót, ale nie.

Jak już zauważyli ludzie (zakładając język, w którym dodawanie jest lewostronne, takie jak C, C ++, C # lub Java), wyrażenie ((1 + 2) + 3)jest dokładnie równoważne z 1 + 2 + 3. Są różne sposoby pisania czegoś w kodzie źródłowym, co miałoby zerowy wpływ na wynikowy kod maszynowy lub kod bajtowy.

Tak czy inaczej, wynikiem będzie instrukcja np. Dodania dwóch rejestrów, a następnie dodania trzeciego lub pobrania dwóch wartości ze stosu, dodania go, wypchnięcia z powrotem, a następnie wzięcia go i dodania innych, lub dodania trzech rejestrów w pojedyncza operacja lub inny sposób zsumowania trzech liczb w zależności od tego, co jest najbardziej sensowne na następnym poziomie (kod maszynowy lub kod bajtowy). W przypadku kodu bajtowego to z kolei prawdopodobnie ulegnie podobnej przebudowie w tym, że np. Równoważnik IL tego (który byłby serią ładunków do stosu, i popping par, aby dodać, a następnie odepchnąć wynik) nie spowodowałoby bezpośredniej kopii tej logiki na poziomie kodu maszynowego, ale coś bardziej sensownego dla danej maszyny.

Ale w twoim pytaniu jest coś więcej.

W przypadku każdego rozsądnego kompilatora C, C ++, Java lub C # spodziewałbym się, że wyniki obu podanych instrukcji będą miały dokładnie takie same wyniki jak:

int a = 6;

Dlaczego wynikowy kod miałby marnować czas na matematykę literałów? Żadne zmiany w stanie programu nie zatrzymają wyniku 1 + 2 + 3bycia 6, więc to powinno być w wykonywanym kodzie. Rzeczywiście, może nawet nie to (w zależności od tego, co zrobisz z tym 6, może uda nam się wyrzucić całość; a nawet C # z jego filozofią „nie optymalizuj mocno, ponieważ jitter i tak to zoptymalizuje” albo wytworzy ekwiwalent int a = 6lub po prostu wyrzuć to wszystko jako niepotrzebne).

To jednak prowadzi nas do możliwego rozszerzenia twojego pytania. Rozważ następujące:

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

i

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(Uwaga: te dwa ostatnie przykłady nie są poprawne w C #, podczas gdy wszystko inne tutaj jest, i są poprawne w C, C ++ i Java.)

Tutaj znowu mamy dokładnie równoważny kod pod względem wydajności. Ponieważ nie są to wyrażenia stałe, nie będą obliczane w czasie kompilacji. Możliwe, że jedna forma jest szybsza od drugiej. Który jest szybszy? Zależy to od procesora i być może od pewnych raczej dowolnych różnic w stanie (zwłaszcza, że ​​jeśli ktoś jest szybszy, prawdopodobnie nie będzie dużo szybszy).

I nie są one całkowicie niezwiązane z twoim pytaniem, ponieważ dotyczą głównie różnic w kolejności, w jakiej coś jest koncepcyjnie zrobione.

W każdym z nich można podejrzewać, że jedno może być szybsze od drugiego. Pojedyncze dekrementy mogą mieć wyspecjalizowane instrukcje, więc (b / 2)--rzeczywiście mogą być szybsze niż (b - 2) / 2. d * 32być może można by go wyprodukować szybciej, zmieniając go w d << 5taki sposób, aby był d * 32 - dszybszy niż d * 31. Różnice między dwoma ostatnimi są szczególnie interesujące; jeden pozwala w niektórych przypadkach na pominięcie przetwarzania, ale drugi pozwala uniknąć błędnego przewidywania gałęzi.

Pozostaje nam zatem dwa pytania: 1. Czy jedno jest rzeczywiście szybsze od drugiego? 2. Czy kompilator przekształci wolniejszy w szybszy?

A odpowiedź brzmi 1. To zależy. 2. Może

Lub, aby rozwinąć, zależy to, ponieważ zależy od danego procesora. Z pewnością istniały procesory, w których naiwny ekwiwalent kodu maszynowego jednego byłby szybszy niż naiwny ekwiwalent kodu maszynowego drugiego. W ciągu historii komputerów elektronicznych nie było też takiego, który byłby zawsze szybszy (element przewidywania błędnych rozgałęzień nie był szczególnie istotny dla wielu, gdy niepoprawne procesory były częstsze).

A może dlatego, że istnieje wiele różnych optymalizacji, które wykonają kompilatory (i fluktuacje i silniki skryptów), i chociaż niektóre mogą być wymagane w niektórych przypadkach, zazwyczaj będziemy w stanie znaleźć niektóre logicznie równoważne kody, które nawet najbardziej naiwny kompilator ma dokładnie takie same wyniki i niektóre logicznie równoważne kody, w których nawet najbardziej wyrafinowany produkuje szybszy kod dla jednego niż dla drugiego (nawet jeśli musimy napisać coś całkowicie patologicznego, aby udowodnić swój punkt).

To może wydawać się bardzo małym problemem optymalizacji,

Nie. Nawet przy bardziej skomplikowanych różnicach niż te, które tu przedstawiam, wydaje się to absolutnie drobiazgową troską, która nie ma nic wspólnego z optymalizacją. Jeśli tak, to kwestia pesymizacji, ponieważ podejrzewasz, że trudniejsze do odczytania ((1 + 2) + 3może być wolniejsze niż łatwiejsze do odczytania 1 + 2 + 3.

ale wybranie C ++ zamiast C # / Java / ... polega na optymalizacji (IMHO).

Jeśli o to właśnie chodziło o wybranie C ++ zamiast C # lub Javy, powiedziałbym, że ludzie powinni wypalić swoją kopię Stroustrup i ISO / IEC 14882 i zwolnić miejsce na kompilatorze C ++, aby zostawić miejsce na więcej plików MP3 lub coś w tym rodzaju.

Te języki mają różne zalety względem siebie.

Jedną z nich jest to, że C ++ jest generalnie szybszy i lżejszy pod względem zużycia pamięci. Tak, istnieją przykłady, w których C # i / lub Java są szybsze i / lub mają lepsze wykorzystanie pamięci przez cały okres użytkowania aplikacji, i stają się one coraz powszechniejsze wraz z poprawą technologii, ale nadal możemy spodziewać się, że przeciętny program napisany w C ++ będzie mniejszy plik wykonywalny, który działa szybciej i zużywa mniej pamięci niż odpowiednik w jednym z tych dwóch języków.

To nie jest optymalizacja.

OptymalizacjaCzasami oznacza „przyspieszenie”. Jest to zrozumiałe, ponieważ często, gdy naprawdę mówimy o „optymalizacji”, naprawdę mówimy o przyspieszeniu, a więc jedno stało się skrótem dla drugiego i przyznam, że sam niewłaściwie używam tego słowa.

Prawidłowe słowo „przyspieszanie” nie oznacza optymalizacji . Prawidłowe słowo to poprawa . Jeśli zmienisz program, a jedyną znaczącą różnicą jest to, że jest on teraz szybszy, nie jest w żaden sposób zoptymalizowany, jest po prostu lepszy.

Optymalizacja polega na wprowadzeniu ulepszeń w odniesieniu do konkretnego aspektu i / lub konkretnego przypadku. Typowe przykłady to:

  1. Jest teraz szybszy dla jednego przypadku użycia, ale wolniejszy dla innego.
  2. Jest teraz szybszy, ale zużywa więcej pamięci.
  3. Jest teraz lżejszy w pamięci, ale wolniejszy.
  4. Jest teraz szybszy, ale trudniejszy w utrzymaniu.
  5. Teraz jest łatwiejszy w utrzymaniu, ale wolniejszy.

Takie przypadki byłyby uzasadnione, jeśli np .:

  1. Szybszy przypadek użycia jest na początku bardziej powszechny lub poważniej utrudniony.
  2. Program był niedopuszczalnie wolny i mamy dużo wolnej pamięci RAM.
  3. Program się zatrzymał, ponieważ zużywał tak dużo pamięci RAM, że spędzał więcej czasu na zamianie niż na wykonywanie superszybkiego przetwarzania.
  4. Program był niedopuszczalnie wolny, a trudniejszy do zrozumienia kod jest dobrze udokumentowany i względnie stabilny.
  5. Program jest wciąż akceptowalnie szybki, a bardziej zrozumiała baza kodu jest tańsza w utrzymaniu i pozwala na łatwiejsze wprowadzanie innych ulepszeń.

Ale takie przypadki nie byłyby również uzasadnione w innych scenariuszach: kod nie został ulepszony przez absolutną nieomylną miarę jakości, został ulepszony pod szczególnym względem, co czyni go bardziej odpowiednim do określonego zastosowania; zoptymalizowany.

Wybór języka ma tutaj wpływ, ponieważ może to mieć wpływ na szybkość, zużycie pamięci i czytelność, ale może to również wpływać na kompatybilność z innymi systemami, dostępność bibliotek, dostępność środowisk uruchomieniowych, dojrzałość tych środowisk uruchomieniowych w danym systemie operacyjnym (z powodu moich grzechów w pewnym sensie skończyłem z Linuksem i Androidem jako moim ulubionym systemem operacyjnym i C # jako moim ulubionym językiem, i chociaż Mono jest świetny, ale nadal dość często go spotykam).

Powiedzenie „wybór C ++ zamiast C # / Java / ... dotyczy optymalizacji” ma sens tylko wtedy, gdy uważasz, że C ++ jest do bani, ponieważ optymalizacja polega na „lepszym, pomimo…”, a nie „lepszym”. Jeśli uważasz, że C ++ jest lepszy mimo wszystko, to ostatnią rzeczą, której potrzebujesz, jest martwienie się o tak małe możliwe mikroopty. Rzeczywiście, prawdopodobnie lepiej w ogóle porzucić to; happy hakerzy to także jakość, którą można zoptymalizować!

Jeśli jednak skłaniasz się do powiedzenia „Kocham C ++, a jedną z rzeczy, które kocham w tym jest wyciskanie dodatkowych cykli”, to jest inna sprawa. Wciąż jest tak, że mikroopty są tego warte tylko wtedy, gdy mogą być nawykiem zwrotnym (tzn. Sposób, w jaki kodujesz w naturalny sposób, będzie szybszy niż wolniejszy). W przeciwnym razie nie są nawet przedwczesną optymalizacją, są przedwczesną pesymizacją, która tylko pogarsza sytuację.

Jon Hanna
źródło
0

Nawiasy służą kompilatorowi do określenia, w jakiej kolejności wyrażenia powinny być oceniane. Czasami są bezużyteczne (z wyjątkiem, że poprawiają lub pogarszają czytelność), ponieważ określają kolejność, która i tak zostanie użyta. Czasami zmieniają kolejność. W

int a = 1 + 2 + 3;

praktycznie każdy istniejący język ma zasadę, że suma jest obliczana przez dodanie 1 + 2, a następnie dodanie wyniku plus 3. Jeśli napisałeś

int a = 1 + (2 + 3);

wtedy nawias wymusiłby inną kolejność: Najpierw dodaj 2 + 3, a następnie dodaj 1 plus wynik. Twój przykład w nawiasach tworzy tę samą kolejność, która i tak zostałaby utworzona. Teraz w tym przykładzie kolejność operacji jest nieco inna, ale sposób, w jaki działa dodawanie liczb całkowitych, wynik jest taki sam. W

int a = 10 - (5 - 4);

nawiasy są krytyczne; ich pominięcie zmieni wynik z 9 na 1.

Po ustaleniu przez kompilator, jakie operacje są wykonywane w jakiej kolejności, nawiasy są całkowicie zapomniane. Jedyne, co kompilator pamięta w tym momencie, to jakie operacje wykonać w jakiej kolejności. Więc tak naprawdę nie ma tu nic, co mógłby zoptymalizować kompilator, nawiasy zniknęły .

gnasher729
źródło
practically every language in existence; oprócz APL: Spróbuj (tutaj) [tryapl.org] wpisując (1-2)+3(2), 1-(2+3)(-4) i 1-2+3(także -4).
tomsmeding
0

Zgadzam się jednak z większością tego, co zostało powiedziane, jednak… nadrzędnym jest to, że nawiasy mają wymuszać kolejność działania… co kompilator absolutnie robi. Tak, produkuje kod maszynowy… ale nie o to chodzi i nie o to pytamy.

Nawiasy rzeczywiście zniknęły: jak już powiedziano, nie są częścią kodu maszynowego, który jest cyframi i niczym innym. Kod zestawu nie jest kodem maszynowym, jest czytelny dla człowieka i zawiera instrukcje według nazwy - nie opcode. Maszyna uruchamia tak zwane opcodes - numeryczne reprezentacje języka asemblera.

Języki takie jak Java należą do obszaru pośredniego, ponieważ kompilują się tylko częściowo na maszynie, która je produkuje. Są one kompilowane do kodu specyficznego dla komputera na komputerze, który je uruchamia, ale to nie ma znaczenia dla tego pytania - nawiasy nadal znikają po pierwszej kompilacji.

Jinzai
źródło
1
Nie jestem pewien, czy to odpowiada na pytanie. Akapity pomocnicze są bardziej mylące niż pomocne. W jaki sposób kompilator Java jest odpowiedni dla kompilatora C ++?
Adam Zuckerman
OP zapytał, czy nawiasy zniknęły… Powiedziałem, że tak, i wyjaśniłem dalej, że kod wykonywalny to tylko liczby reprezentujące kody. Java została podniesiona w innej odpowiedzi. Myślę, że dobrze odpowiada na pytanie… ale to tylko moja opinia. Dzięki za odpowiedź.
jinzai
3
Nawiasy nie wymuszają „kolejności działania”. Zmieniają pierwszeństwo. Tak więc, w a = f() + (g() + h());kompilator jest wolna zadzwonić f, gi hw tej kolejności (lub w dowolnej kolejności to podoba).
Alok
Nie zgadzam się z tym stwierdzeniem… absolutnie możesz wymusić kolejność działania za pomocą nawiasów.
jinzai
0

Kompilatory, niezależnie od języka, tłumaczą całą matematyczną poprawkę na postfiks. Innymi słowy, gdy kompilator widzi coś takiego:

((a+b)+c)

przekłada to na to:

 a b + c +

Dzieje się tak, ponieważ chociaż notacja poprawki jest łatwiejsza do odczytania, notacja poprawki jest znacznie bliższa faktycznym krokom, jakie komputer musi wykonać, aby wykonać zadanie (oraz ponieważ istnieje już dobrze opracowany algorytm). definicja, postfix eliminuje wszystkie problemy z kolejnością operacji lub nawiasami, co oczywiście znacznie ułatwia pisanie kodu maszynowego.

Polecam artykuł w Wikipedii na temat odwrotnej notacji polskiej, aby uzyskać więcej informacji na ten temat.

Brian Drozd
źródło
5
To niepoprawne założenie dotyczące sposobu, w jaki kompilatory tłumaczą operacje. Zakładasz na przykład maszynę stosową. Co jeśli masz procesor wektorowy? co jeśli masz maszynę z dużą liczbą rejestrów?
Ahmed Masud