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).
c++
optimization
compilation
Serge
źródło
źródło
Odpowiedzi:
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).
źródło
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.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.źródło
int a = 8;// = 2*3 + 5
int five = 7; //HR made us change this to six...
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!
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.
źródło
a
naprawdę można niepodpisać, prowadzenie obliczeń z-a + b
może łatwo przepełnić, jeślia
byłyby negatywne ib
pozytywne.(b+c)
w ostatnim wierszu będzie promować swoje argumentyint
, więc jeśli kompilator nie zdefiniujeint
16 bitów (albo dlatego, że jest starożytny, albo wymierza mały mikrokontroler), ostatni wiersz byłby całkowicie uzasadniony.int
zostanie awansowane,int
chyba że ten typ nie byłby w stanie przedstawić wszystkich swoich wartości, w którym to przypadku zostałby awansowanyunsigned 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. Gdybyint
jednak typ 16-bitowy był uwięziony w wyniku przepełnienia, byłyby przypadki, w których jedno z wyrażeń ...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ć ...
... kompilator może emitować coś takiego:
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!
źródło
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.
źródło
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.
źródło
Oba kody kończą na sztywno jako 6:
Sprawdź tutaj
źródło
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 z1 + 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:
Dlaczego wynikowy kod miałby marnować czas na matematykę literałów? Żadne zmiany w stanie programu nie zatrzymają wyniku
1 + 2 + 3
bycia6
, 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 ekwiwalentint a = 6
lub po prostu wyrzuć to wszystko jako niepotrzebne).To jednak prowadzi nas do możliwego rozszerzenia twojego pytania. Rozważ następujące:
i
(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 * 32
być może można by go wyprodukować szybciej, zmieniając go wd << 5
taki sposób, aby byłd * 32 - d
szybszy 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).
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) + 3
może być wolniejsze niż łatwiejsze do odczytania1 + 2 + 3
.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:
Takie przypadki byłyby uzasadnione, jeśli np .:
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ę.
źródło
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
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ś
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
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 .
ź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) i1-2+3
(także -4).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.
źródło
a = f() + (g() + h());
kompilator jest wolna zadzwonićf
,g
ih
w tej kolejności (lub w dowolnej kolejności to podoba).Kompilatory, niezależnie od języka, tłumaczą całą matematyczną poprawkę na postfiks. Innymi słowy, gdy kompilator widzi coś takiego:
przekłada to na to:
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.
źródło