Co potwierdza twierdzenie, że C ++ może być szybszy niż JVM lub CLR z JIT? [Zamknięte]


119

Powtarzającym się motywem na SE, który zauważyłem w wielu pytaniach, jest ciągły argument, że C ++ jest szybszy i / lub bardziej wydajny niż języki wyższego poziomu, takie jak Java. Kontrargumentem jest to, że współczesna JVM lub CLR może być równie wydajna dzięki JIT i tak dalej w przypadku rosnącej liczby zadań, a C ++ jest jeszcze bardziej wydajny, jeśli wiesz, co robisz i dlaczego robisz to w określony sposób zasłuży na wzrost wydajności. To oczywiste i ma sens.

Chciałbym poznać podstawowe wyjaśnienie (jeśli istnieje coś takiego ...), dlaczego i jak niektóre zadania są szybsze w C ++ niż JVM lub CLR? Czy to po prostu dlatego, że C ++ jest skompilowany w kodzie maszynowym, podczas gdy JVM lub CLR nadal mają narzut przetwarzania podczas kompilacji JIT?

Kiedy próbuję zbadać ten temat, znajduję tylko te same argumenty, które przedstawiłem powyżej, bez żadnych szczegółowych informacji na temat dokładnego zrozumienia, w jaki sposób C ++ można wykorzystać do obliczeń o wysokiej wydajności.


Wydajność zależy również od złożoności programu.
pandu,

23
Dodam, że „C ++ jest jeszcze bardziej wydajny, jeśli wiesz, co robisz i dlaczego robienie pewnych czynności zasługuje na wzrost wydajności”. mówiąc, że to nie tylko kwestia wiedzy, to kwestia czasu programisty. Maksymalizacja optymalizacji nie zawsze jest skuteczna. Właśnie dlatego istnieją języki wyższego poziomu, takie jak Java i Python (między innymi) - aby skrócić czas, jaki programiści muszą poświęcić programowaniu na wykonanie danego zadania kosztem wysoce dostrojonej optymalizacji.
Joel Cornett,

4
@Jel Cornett: Całkowicie się zgadzam. Jestem zdecydowanie bardziej produktywny w Javie niż w C ++ i rozważam C ++ tylko wtedy, gdy muszę napisać naprawdę szybki kod. Z drugiej strony widziałem, że źle napisany kod C ++ jest naprawdę wolny: C ++ jest mniej przydatny w rękach niewykwalifikowanych programistów.
Giorgio

3
Każde wyjście kompilacji, które może być wygenerowane przez JIT, może być wygenerowane przez C ++, ale kod, który C ++ może wytworzyć, niekoniecznie może być wygenerowany przez JIT. Tak więc możliwości i cechy wydajności C ++ są nadzbiorem możliwości dowolnego języka wyższego poziomu. QED
tylerl

1
@Doval Technicznie prawdziwe, ale z reguły z jednej strony można policzyć możliwe czynniki uruchomieniowe wpływające na wydajność programu. Zwykle bez użycia więcej niż dwóch palców. W najgorszym przypadku wysyłasz wiele plików binarnych ... z tym, że okazuje się, że nawet nie musisz tego robić, ponieważ potencjalne przyspieszenie jest znikome, dlatego nikt nawet się tym nie przejmuje.
tylerl

Odpowiedzi:


200

Chodzi o pamięć (nie JIT). „Przewaga JIT nad C” ogranicza się głównie do optymalizacji połączeń wirtualnych lub nie-wirtualnych poprzez inline, coś, nad czym CPU BTB już ciężko pracuje.

W nowoczesnych maszynach dostęp do pamięci RAM jest naprawdę powolny (w porównaniu do wszystkiego, co robi procesor), co oznacza, że ​​aplikacje wykorzystujące pamięci podręczne w jak największym stopniu (co jest łatwiejsze, gdy używana jest mniejsza pamięć) mogą być nawet sto razy szybsze niż te, które nie. Istnieje wiele sposobów, w jakie Java wykorzystuje więcej pamięci niż C ++ i utrudnia pisanie aplikacji w pełni wykorzystujących pamięć podręczną:

  • Na każdy obiekt przypada co najmniej 8 bajtów, a użycie obiektów zamiast prymitywów jest wymagane lub preferowane w wielu miejscach (mianowicie kolekcjach standardowych).
  • Ciągi składają się z dwóch obiektów i mają narzut 38 bajtów
  • UTF-16 jest używany wewnętrznie, co oznacza, że ​​każdy znak ASCII wymaga dwóch bajtów zamiast jednego (Oracle JVM niedawno wprowadził optymalizację, aby tego uniknąć dla czystych ciągów ASCII).
  • Nie ma zagregowanego typu odniesienia (tj. Struktury), a z kolei nie ma tablic zagregowanych typów odniesienia. Obiekt Java lub tablica obiektów Java ma bardzo słabą lokalizację pamięci podręcznej L1 / L2 w porównaniu do struktur C i tablic.
  • Generycy Java używają kasowania typu, który ma słabą lokalizację pamięci podręcznej w porównaniu do tworzenia instancji typu.
  • Przydział obiektów jest nieprzejrzysty i musi być wykonany osobno dla każdego obiektu, więc aplikacja nie może celowo rozłożyć danych w sposób przyjazny dla pamięci podręcznej i nadal traktować je jako dane ustrukturyzowane.

Niektóre inne czynniki związane z pamięcią, ale nie z pamięcią podręczną:

  • Nie ma alokacji stosu, więc wszystkie nieprymitywne dane, z którymi pracujesz, muszą znajdować się na stercie i przechodzić proces wyrzucania elementów bezużytecznych (niektóre najnowsze zespoły JIT w niektórych przypadkach dokonują alokacji stosu za sceną).
  • Ponieważ nie ma zagregowanych typów odniesień, nie ma przekazywania stosu zagregowanych typów odniesień. (Pomyśl o wydajnym przekazywaniu argumentów Vector)
  • Odśmiecanie może zaszkodzić zawartości pamięci podręcznej L1 / L2, a GC stop-the-pauz przerywa interakcję.
  • Konwersja między typami danych zawsze wymaga kopiowania; nie możesz wziąć wskaźnika do wiązki bajtów, które otrzymałeś z gniazda i zinterpretować je jako zmiennoprzecinkowe.

Niektóre z tych rzeczy to kompromisy (brak konieczności ręcznego zarządzania pamięcią jest warty rezygnacji z dużej wydajności dla większości ludzi), niektóre są prawdopodobnie wynikiem starań, aby Java była prosta, a niektóre są błędami projektowymi (choć być może tylko z perspektywy czasu , a mianowicie UTF-16 był kodowaniem o stałej długości podczas tworzenia Java, co sprawia, że ​​decyzja o jego wyborze jest znacznie bardziej zrozumiała).

Warto zauważyć, że wiele z tych kompromisów jest bardzo różnych dla Java / JVM niż dla C # / CIL. .NET CIL ma struktury typu referencyjnego, alokację / przekazywanie stosu, spakowane tablice struktur i generyczne wystąpienia instancji typu.


37
+1 - ogólnie rzecz biorąc, to dobra odpowiedź. Nie jestem jednak pewien, czy punktor „nie ma alokacji stosu” jest całkowicie dokładny. Java JIT często wykonują analizę zmiany znaczenia, aby umożliwić alokację stosu tam, gdzie to możliwe - być może powinieneś powiedzieć, że język Java nie pozwala programiście decydować, kiedy obiekt jest alokowany na stosie, czy na stosie. Ponadto, jeśli używany jest generacyjny moduł wyrzucania elementów bezużytecznych (którego używają wszystkie współczesne maszyny JVM), „alokacja sterty” oznacza zupełnie inną rzecz (o zupełnie innej charakterystyce wydajności) niż w środowisku C ++.
Daniel Pryden,

5
Sądzę, że są dwie inne rzeczy, ale głównie pracuję z rzeczami na znacznie wyższym poziomie, więc powiedz, czy się mylę. Naprawdę nie możesz napisać C ++ bez rozwinięcia bardziej ogólnej świadomości tego, co faktycznie dzieje się w pamięci i tego, jak działa kod maszynowy, podczas gdy skrypty lub języki maszyn wirtualnych odciągają to wszystko od twojej uwagi. Masz również znacznie bardziej szczegółową kontrolę nad tym, jak to działa, podczas gdy w maszynie wirtualnej lub w języku interpretowanym polegasz na tym, co autorzy bibliotek głównych zoptymalizowali pod kątem zbyt specyficznego scenariusza.
Erik Reppen

18
+1. Dodam jeszcze jedną rzecz (ale nie chcę przesyłać nowej odpowiedzi): indeksowanie tablic w Javie zawsze obejmuje sprawdzanie granic. W przypadku C i C ++ tak nie jest.
riwalk

7
Warto zauważyć, że alokacja sterty Java jest znacznie szybsza niż naiwna wersja z C ++ (ze względu na wewnętrzne buforowanie i inne rzeczy), ale alokacja pamięci w C ++ może być znacznie lepsza, jeśli wiesz, co robisz.
Brendan Long

10
@BrendanLong, prawda .. ale tylko wtedy, gdy pamięć jest czysta - gdy aplikacja działa przez pewien czas, przydzielanie pamięci będzie wolniejsze z powodu potrzeby GC, która znacznie spowalnia pracę, ponieważ musi zwolnić pamięć, uruchomić finalizatory, a następnie kompaktowy. Jest to kompromis, który przynosi korzyści w testach porównawczych, ale (IMHO) ogólnie spowalnia aplikacje.
gbjbaanb

67

Czy to po prostu dlatego, że C ++ jest kompilowany w asemblerze / kodzie maszynowym, podczas gdy Java / C # wciąż ma narzut przetwarzania kompilacji JIT w czasie wykonywania?

Częściowo, ale ogólnie, zakładając absolutnie fantastyczny najnowocześniejszy kompilator JIT, właściwy kod C ++ nadal działa lepiej niż kod Java z DWÓCH głównych powodów:

1) Szablony C ++ zapewniają lepsze możliwości pisania kodu, który jest zarówno ogólny, jak i wydajny . Szablony zapewniają programistom C ++ bardzo przydatną abstrakcję, która ma narzut ZERO. (Szablony są w zasadzie pisaniem kaczych w czasie kompilacji). Dla kontrastu, najlepsze, co można uzyskać dzięki rodzajom Java, to zasadniczo funkcje wirtualne. Funkcje wirtualne zawsze mają narzut związany z czasem działania i generalnie nie można ich wstawiać.

Ogólnie rzecz biorąc, większość języków, w tym Java, C #, a nawet C, umożliwia wybór między wydajnością a ogólnością / abstrakcją. Szablony C ++ dają oba (kosztem dłuższych czasów kompilacji).

2) Fakt, że standard C ++ nie ma wiele do powiedzenia na temat układu binarnego skompilowanego programu C ++, daje kompilatorom C ++ znacznie większą swobodę niż kompilator Java, pozwalając na lepszą optymalizację (kosztem większych trudności w debugowaniu czasami). ) W rzeczywistości sama specyfikacja języka Java wymusza obniżenie wydajności w niektórych obszarach. Na przykład nie można mieć ciągłej tablicy obiektów w Javie. Możesz mieć tylko ciągłą tablicę wskaźników obiektów(referencje), co oznacza, że ​​iteracja po tablicy w Javie zawsze pociąga za sobą koszty pośrednie. Jednak semantyka wartości C ++ umożliwia tablice ciągłe. Inną różnicą jest fakt, że C ++ pozwala na przydzielanie obiektów na stosie, podczas gdy Java nie, co oznacza, że ​​w praktyce, ponieważ większość programów C ++ ma tendencję do przydzielania obiektów na stosie, koszt przydziału jest często bliski zeru.

Jednym z obszarów, w którym C ++ może pozostawać w tyle za Javą, jest każda sytuacja, w której wiele małych obiektów musi zostać przydzielonych na stercie. W takim przypadku system śmieciowy Javy prawdopodobnie spowoduje lepszą wydajność niż standardowa newi deletew C ++, ponieważ Java GC umożliwia masowe zwolnienie. Ale znowu, programista C ++ może to zrekompensować za pomocą puli pamięci lub alokatora płyt, podczas gdy programista Java nie ma możliwości skorzystania z wzorca alokacji pamięci, dla którego środowisko Java nie jest zoptymalizowane.

Zobacz także tę doskonałą odpowiedź, aby uzyskać więcej informacji na ten temat.


6
Dobra odpowiedź, ale jedna drobna uwaga: „Szablony C ++ dają oba (kosztem dłuższych czasów kompilacji).” Dodałbym również kosztem większego rozmiaru programu. Nie zawsze może to stanowić problem, ale na pewno może to być programowanie na urządzenia mobilne.
Leo

9
@luiscubal: nie, pod tym względem generyczne C # są bardzo podobne do Javy (pod tym samym, że pobierana jest ta sama „ogólna” ścieżka kodu bez względu na to, jakie typy są przekazywane). Sztuczka do szablonów C ++ polega na tym, że kod jest tworzony raz każdy typ, do którego jest stosowany. Tak std::vector<int>jest dynamiczna tablica zaprojektowana tylko dla ints, a kompilator jest w stanie odpowiednio ją zoptymalizować. AC # List<int>to wciąż tylko List.
czerwiec

12
@jalf C # List<int>używa int[], a nie Object[]jak Java. Zobacz stackoverflow.com/questions/116988/…
luiscubal

5
@luiscubal: twoja terminologia jest niejasna. JIT nie działa zgodnie z tym, co uważam za „czas kompilacji”. Oczywiście masz rację, biorąc pod uwagę wystarczająco sprytny i agresywny kompilator JIT, nie ma praktycznie żadnych ograniczeń co do tego, co mógłby zrobić. Ale C ++ wymaga takiego zachowania. Ponadto szablony C ++ pozwalają programiście na określenie wyraźnych specjalizacji, umożliwiając dodatkowe jawne optymalizacje, tam gdzie ma to zastosowanie. C # nie ma na to odpowiednika. Na przykład w C ++ mógłbym zdefiniować, vector<N>gdzie, w konkretnym przypadku vector<4>, należy użyć mojej ręcznie kodowanej implementacji SIMD
czerwiec

5
@Leo: Nadęty kod przez szablony był problemem 15 lat temu. Dzięki intensywnemu tworzeniu szablonów i inline, a także kompilatorom umiejętności, które zostały zebrane, ponieważ (podobnie jak składanie identycznych instancji), obecnie wiele szablonów zmniejsza się dzięki szablonom.
sbi

46

To, czego inne odpowiedzi (jak dotąd 6) zapomniałem wspomnieć, ale uważam za bardzo ważne, aby odpowiedzieć na to pytanie, jest jedną z bardzo podstawowych filozofii projektowych C ++, którą sformułował i zastosował Stroustrup od pierwszego dnia:

Nie płacisz za to, czego nie używasz.

Istnieją inne ważne podstawowe zasady projektowania, które znacznie ukształtowały C ++ (takie, że nie powinieneś być zmuszany do określonego paradygmatu), ale nie płacisz za to, czego nie używasz, jest jednym z najważniejszych.


W swojej książce The Design and Evolution of C ++ (zwykle określanej jako [D&E]) Stroustrup opisuje, jakie były potrzeby, które sprawiły, że wymyślił C ++. Moimi własnymi słowami: W swojej pracy doktorskiej (związanej z symulacjami sieci, IIRC) zaimplementował system w SIMULI, który bardzo mu się podobał, ponieważ język był bardzo dobry, pozwalając mu wyrażać swoje myśli bezpośrednio w kodzie. Jednak powstały program działał o wiele za wolno i aby uzyskać dyplom, przepisał rzecz w BCPL, poprzedniku C. Pisanie kodu w BCPL opisuje jako ból, ale wynikowy program był wystarczająco szybki, aby dostarczyć wyniki, które pozwoliły mu ukończyć doktorat.

Potem chciał języka, który umożliwiłby przetłumaczenie rzeczywistych problemów na kod tak bezpośrednio, jak to możliwe, ale także pozwoliłby, aby kod był bardzo wydajny.
W tym celu stworzył coś, co później stanie się C ++.


Tak więc powyższy cel nie jest tylko jedną z kilku podstawowych zasad projektowania, jest bardzo zbliżony do racji bytu C ++. I można go znaleźć prawie wszędzie w języku: funkcje są tylko virtualwtedy, gdy chcesz (ponieważ wywoływanie funkcji wirtualnych wiąże się z niewielkim narzutem) POD są inicjowane automatycznie, gdy wyraźnie o to poprosisz, wyjątki kosztują tylko wydajność, gdy faktycznie wyrzuć je (podczas gdy było to jawnym celem projektowym, aby konfiguracja / czyszczenie ramek stosów było bardzo tanie), brak GC działającego, kiedy ma na to ochotę itp.

C ++ wyraźnie postanowił nie zapewniać ci pewnych udogodnień („czy muszę tutaj uczynić tę metodę wirtualną?”) W zamian za wydajność („nie, nie wiem, a teraz kompilator może inlineto zrobić i zoptymalizować cała sprawa! ”) i, co nie jest zaskoczeniem, rzeczywiście przyniosło to wzrost wydajności w porównaniu z wygodniejszymi językami.


4
Nie płacisz za to, czego nie używasz. =>, a następnie dodali RTTI :(
Matthieu M.,

11
@ Matthieu: Rozumiem twoje zdanie, ale nie mogę nie zauważyć, że nawet to zostało dodane z troską o wydajność. RTTI jest tak określony, że można go zaimplementować przy użyciu wirtualnych tabel, a zatem dodaje bardzo niewiele narzutu, jeśli go nie używasz. Jeśli nie użyjesz polimorfizmu, nie będzie żadnych kosztów. Czy coś brakuje?
sbi

9
@Matthieu: Oczywiście jest powód. Ale czy ten powód jest racjonalny? Z tego, co widzę, „koszt RTTI”, jeśli nie jest używany, jest dodatkowym wskaźnikiem w wirtualnej tabeli każdej klasy polimorficznej, wskazując na jakiś obiekt RTTI gdzieś statycznie przydzielony. Jeśli nie chcesz zaprogramować mikroukładu w moim tosterze, jak może to mieć znaczenie?
sbi

4
@Aaronaught: Brakuje mi odpowiedzi na to pytanie. Czy naprawdę odrzuciłeś moją odpowiedź, ponieważ wskazuje ona na podstawową filozofię, dzięki której Stroustrup i wsp. Dodali funkcje w sposób umożliwiający wydajność, zamiast wymieniać te sposoby i funkcje indywidualnie?
sbi

9
@Aaronaught: Masz moją sympatię.
sbi

29

Czy znasz artykuł badawczy Google na ten temat?

Z wniosku:

Okazuje się, że pod względem wydajności C ++ wygrywa z dużym marginesem. Jednak wymagało to również najbardziej intensywnych prac tuningowych, z których wiele wykonano na poziomie wyrafinowania, który nie byłby dostępny dla przeciętnego programisty.

Jest to przynajmniej częściowe wyjaśnienie w sensie „ponieważ kompilatory C ++ w świecie rzeczywistym wytwarzają szybszy kod niż kompilatory Java za pomocą miar empirycznych”.


4
Oprócz różnic w użyciu pamięci i pamięci podręcznej, jedną z najważniejszych jest ilość przeprowadzonej optymalizacji. Porównaj, ile optymalizacji GCC / LLVM (i prawdopodobnie Visual C ++ / ICC) robi w stosunku do kompilatora Java HotSpot: znacznie więcej, szczególnie w odniesieniu do pętli, eliminacji zbędnych gałęzi i alokacji rejestrów. Kompilatory JIT zwykle nie mają czasu na te agresywne optymalizacje, nawet sądząc, że mogłyby je lepiej zaimplementować, korzystając z dostępnych informacji o czasie wykonywania.
Gratian Lup,

2
@GratianLup: Zastanawiam się, czy to (nadal) prawda w przypadku LTO.
Deduplicator

2
@GratianLup: Nie zapominajmy o optymalizacji opartej na profilu dla C ++ ...
Deduplicator

23

To nie jest duplikat twoich pytań, ale zaakceptowana odpowiedź odpowiada na większość twoich pytań: nowoczesna recenzja Javy

Podsumowując:

Zasadniczo semantyka Java wskazuje, że jest to język wolniejszy niż C ++.

Tak więc, w zależności od tego, z jakim innym językiem porównujesz C ++, możesz otrzymać lub nie tę samą odpowiedź.

W C ++ masz:

  • Zdolność do inteligentnego wprowadzania,
  • generyczne generowanie kodu o silnej lokalizacji (szablony)
  • możliwie małe i kompaktowe dane
  • możliwości uniknięcia pośredników
  • przewidywalne zachowanie pamięci
  • optymalizacje kompilatora możliwe tylko ze względu na użycie abstrakcji wysokiego poziomu (szablonów)

Są to cechy lub skutki uboczne definicji języka, która sprawia, że ​​teoretycznie jest bardziej wydajna pod względem pamięci i szybkości niż jakikolwiek język, który:

  • używaj masowo pośrednictwa (języki „wszystko jest zarządzanym odniesieniem / wskaźnikiem”): pośrednie oznacza, że ​​procesor musi skakać w pamięci, aby uzyskać niezbędne dane, zwiększając awarie pamięci podręcznej procesora, co oznacza spowolnienie przetwarzania - C używa również pośrednich a dużo, nawet jeśli może mieć małe dane jako C ++;
  • generuj obiekty o dużych rozmiarach, do których dostęp jest możliwy pośrednio: jest to konsekwencja domyślnego posiadania referencji, członkowie są wskaźnikami, więc kiedy otrzymasz członka, możesz nie dostać danych blisko rdzenia obiektu nadrzędnego, ponownie wyzwalając brak pamięci podręcznej.
  • użyj kolektora Garbarge: po prostu uniemożliwia przewidywalność wydajności (z założenia).

Agresywne wstawianie kompilatora w C ++ zmniejsza lub eliminuje wiele pośrednich. Zdolność do generowania niewielkiego zestawu zwartych danych sprawia, że ​​jest przyjazny dla pamięci podręcznej, jeśli nie rozłożysz tych danych w całej pamięci zamiast je spakować (oba są możliwe, C ++ pozwala tylko wybrać). RAII sprawia, że ​​zachowanie pamięci C ++ jest przewidywalne, co eliminuje wiele problemów w przypadku symulacji w czasie rzeczywistym lub pół-w czasie rzeczywistym, które wymagają dużej prędkości. Problemy związane z lokalizacją można ogólnie podsumować w ten sposób: im mniejszy program / dane, tym szybsze wykonanie. C ++ zapewnia różnorodne sposoby upewnienia się, że twoje dane są tam, gdzie chcesz (w puli, tablicy lub czymkolwiek) i że są zwarte.

Oczywiście istnieją inne języki, które mogą zrobić to samo, ale są one po prostu mniej popularne, ponieważ nie zapewniają tylu narzędzi abstrakcyjnych jak C ++, więc są mniej przydatne w wielu przypadkach.


7

Chodzi głównie o pamięć (jak powiedział Michael Borgwardt) z odrobiną nieefektywności JIT.

Jedną z rzeczy, o których nie wspomniano, jest pamięć podręczna - aby w pełni korzystać z pamięci podręcznej, dane muszą być rozmieszczone w sposób ciągły (tj. Wszystkie razem). Teraz w systemie GC pamięć jest przydzielana na stercie GC, co jest szybkie, ale gdy pamięć się zużyje, GC będzie regularnie uruchamiać i usuwać nieużywane bloki, a następnie kompaktować pozostałe razem. Teraz oprócz oczywistego spowolnienia przenoszenia używanych bloków razem, oznacza to, że dane, których używasz, mogą się nie skleić. Jeśli masz tablicę 1000 elementów, chyba że przydzielisz je wszystkie naraz (a następnie zaktualizujesz ich zawartość zamiast usuwać i tworzyć nowe - które zostaną utworzone na końcu stosu), zostaną one rozrzucone po całym stosie, a zatem wymaga kilku trafień pamięci, aby odczytać je wszystkie w pamięci podręcznej procesora. Aplikacja AC / C ++ najprawdopodobniej przydzieli pamięć dla tych elementów, a następnie zaktualizujesz bloki danymi. (ok, istnieją struktury danych takie jak lista, które zachowują się bardziej jak przydziały pamięci GC, ale ludzie wiedzą, że są one wolniejsze niż wektory).

Możesz to zobaczyć po prostu poprzez zamianę dowolnych obiektów StringBuilder na String ... Konstruktorzy String pracują, wstępnie przydzielając pamięć i wypełniając ją, i jest to znana sztuczka wydajnościowa dla systemów Java / .NET.

Nie zapominaj, że paradygmat „usuń stare i przydziel nowe kopie” jest bardzo intensywnie wykorzystywany w Javie / C #, po prostu dlatego, że ludzie mówią, że alokacje pamięci są naprawdę szybkie z powodu GC, a więc model rozproszonej pamięci jest wszędzie używany ( z wyjątkiem konstruktorów łańcuchów, oczywiście), więc wszystkie biblioteki marnują pamięć i zużywają jej dużo, z których żadna nie korzysta z ciągłości. Za to obwiniaj szum wokół GC - powiedzieli ci, że pamięć jest wolna, lol.

Sam GC jest oczywiście kolejnym hitem - kiedy działa, musi nie tylko zamiatać stertę, ale także musi uwolnić wszystkie nieużywane bloki, a następnie musi uruchomić wszelkie finalizatory (chociaż robiono to osobno Następnym razem z zatrzymaną aplikacją) (nie wiem, czy nadal jest to hit, ale wszystkie czytane przeze mnie dokumenty mówią, że używaj finalizatorów tylko, jeśli to naprawdę konieczne), a następnie musi przesunąć te bloki na pozycję, aby stos był zagęszczony i zaktualizuj odniesienie do nowej lokalizacji bloku. Jak widać, to dużo pracy!

Doskonałe trafienia dla pamięci C ++ sprowadzają się do alokacji pamięci - gdy potrzebujesz nowego bloku, musisz przejść stertę w poszukiwaniu następnego wolnego miejsca, które jest wystarczająco duże, z mocno rozdrobnioną stertą, nie jest to tak szybkie jak GC „po prostu alokuj kolejny blok na końcu”, ale myślę, że nie jest on tak wolny, jak cała praca związana z zagęszczaniem GC, i można go złagodzić za pomocą wielu stosów bloków o stałej wielkości (znanych również jako pule pamięci).

Jest więcej ... jak ładowanie zestawów z GAC, które wymaga sprawdzania bezpieczeństwa, ścieżek sond (włącz sxstrace i po prostu spójrz, co się dzieje!) I innych innych nadinżynierii, które wydają się być znacznie bardziej popularne w java / .net niż C / C ++.


2
Wiele rzeczy, które piszesz, nie jest prawdą w przypadku nowoczesnych generatorów śmieci.
Michael Borgwardt,

3
@MichaelBorgwardt taki jak? Mówię „GC działa regularnie” i „zagęszcza stertę”. Reszta mojej odpowiedzi dotyczy sposobu, w jaki struktury danych aplikacji wykorzystują pamięć.
gbjbaanb

6

„Czy to po prostu dlatego, że C ++ jest kompilowany w asemblerze / kodzie maszynowym, podczas gdy Java / C # wciąż ma narzut przetwarzania kompilacji JIT w czasie wykonywania?” Zasadniczo tak!

Szybka uwaga, Java ma więcej kosztów ogólnych niż tylko kompilacja JIT. Na przykład, robi o wiele więcej sprawdzania dla ciebie (tak to robi rzeczy jak ArrayIndexOutOfBoundsExceptionsi NullPointerExceptions). Śmieciarka to kolejny znaczny narzut.

Jest to dość szczegółowe porównanie tutaj .


2

Pamiętaj, że poniższe zestawienie porównuje jedynie różnicę między kompilacją natywną a kompilacją JIT i nie obejmuje specyfiki konkretnego języka lub frameworka. Mogą istnieć uzasadnione powody, aby wybrać konkretną platformę poza tym.

Gdy twierdzimy, że kod macierzysty jest szybszy, mówimy o typowym przypadku użycia kodu skompilowanego w sposób natywny w porównaniu do kodu skompilowanego w JIT, w którym typowe użycie aplikacji skompilowanej w JIT ma być uruchamiane przez użytkownika, z natychmiastowymi rezultatami (np. Brak najpierw czeka na kompilatorze). W takim przypadku nie sądzę, aby ktokolwiek mógł z prostą miną twierdzić, że skompilowany kod JIT może dopasować lub pokonać kod natywny.

Załóżmy, że mamy program napisany w jakimś języku X i możemy go skompilować za pomocą natywnego kompilatora i ponownie za pomocą kompilatora JIT. Każdy przepływ pracy obejmuje te same etapy, które można uogólnić jako (Kod -> Przedstawiciel pośredni -> Kod maszynowy -> Wykonanie). Duża różnica między dwoma to, które etapy widzi użytkownik, a które programista. W przypadku kompilacji natywnej programista widzi wszystko oprócz etapu wykonania, ale w przypadku rozwiązania JIT kompilacja do kodu maszynowego jest postrzegana przez użytkownika, oprócz wykonywania.

Twierdzenie, że A jest szybsze niż B, odnosi się do czasu potrzebnego na uruchomienie programu, jak widzi użytkownik . Jeśli założymy, że oba fragmenty kodu działają identycznie na etapie wykonywania, musimy założyć, że przepływ pracy JIT jest wolniejszy dla użytkownika, ponieważ musi on także zobaczyć czas T kompilacji do kodu maszynowego, gdzie T> 0. Tak , aby każda możliwość, aby przepływ pracy JIT działał tak samo jak natywny przepływ pracy, dla użytkownika, musimy skrócić czas wykonywania kodu, tak aby wykonanie + kompilacja do kodu maszynowego były niższe niż tylko etap wykonania natywnego przepływu pracy. Oznacza to, że musimy lepiej zoptymalizować kod w kompilacji JIT niż w kompilacji natywnej.

Jest to jednak raczej niewykonalne, ponieważ aby wykonać niezbędne optymalizacje w celu przyspieszenia wykonywania, musimy poświęcić więcej czasu na kompilację do etapu kodu maszynowego, a zatem za każdym razem, gdy zaoszczędzimy w wyniku zoptymalizowania kodu, zostanie utracony, ponieważ dodajemy go do kompilacji. Innymi słowy, „powolność” rozwiązania opartego na JIT nie wynika wyłącznie z dodatkowego czasu na kompilację JIT, ale kod wygenerowany przez tę kompilację działa wolniej niż rozwiązanie rodzime.

Posłużę się przykładem: Zarejestruj przydział. Ponieważ dostęp do pamięci jest kilka tysięcy razy wolniejszy niż dostęp do rejestrów, najlepiej, gdy jest to możliwe, chcemy korzystać z rejestrów i mieć jak najmniej dostępów do pamięci, ale mamy ograniczoną liczbę rejestrów i musimy przelać stan do pamięci, gdy jest to potrzebne rejestr. Jeśli użyjemy algorytmu alokacji rejestru, którego obliczenie zajmuje 200 ms, w wyniku czego zaoszczędzimy 2 ms czasu wykonania - nie wykorzystujemy najlepiej czasu kompilatora JIT. Rozwiązania takie jak algorytm Chaitin, który może generować wysoce zoptymalizowany kod, są nieodpowiednie.

Rolą kompilatora JIT jest jednak zachowanie najlepszej równowagi między czasem kompilacji a jakością produkowanego kodu, z dużym naciskiem na szybki czas kompilacji, ponieważ nie chcesz, aby użytkownik czekał. Wydajność wykonywanego kodu jest mniejsza w przypadku JIT, ponieważ natywny kompilator nie jest związany (dużo) czasem w optymalizowaniu kodu, więc można swobodnie korzystać z najlepszych algorytmów. Możliwość, że ogólna kompilacja + wykonanie kompilatora JIT może pobić tylko czas wykonania natywnie skompilowanego kodu, wynosi 0.

Ale nasze maszyny wirtualne nie ograniczają się tylko do kompilacji JIT. Korzystają z technik kompilacji Ahead-of-time, buforowania, wymiany na gorąco i optymalizacji adaptacyjnych. Zmodyfikujmy więc nasze twierdzenie, że wydajność jest tym, co widzi użytkownik, i ograniczmy ją do czasu potrzebnego na wykonanie programu (załóżmy, że skompilowaliśmy AOT). Możemy skutecznie sprawić, aby kod wykonawczy był równoważny natywnemu kompilatorowi (a może lepiej?). Wielkim twierdzeniem dla maszyn wirtualnych jest to, że mogą one być w stanie wytworzyć kod lepszej jakości niż natywny kompilator, ponieważ ma on dostęp do większej ilości informacji - informacji o uruchomionym procesie, takich jak częstotliwość wykonywania określonej funkcji. Maszyna wirtualna może następnie zastosować adaptacyjne optymalizacje do najbardziej niezbędnego kodu za pomocą wymiany na gorąco.

Jest jednak problem z tym argumentem - zakłada on, że optymalizacja sterowana profilem i tym podobne jest czymś wyjątkowym dla maszyn wirtualnych, co nie jest prawdą. Możemy zastosować go również do kompilacji natywnej - kompilując naszą aplikację z włączonym profilowaniem, rejestrując informacje, a następnie ponownie kompilując aplikację z tym profilem. Prawdopodobnie warto również zauważyć, że wymiana kodu na gorąco nie jest czymś, co może zrobić tylko kompilator JIT, możemy to zrobić dla kodu natywnego - chociaż rozwiązania oparte na JIT są łatwiej dostępne i znacznie łatwiejsze dla programisty. Główne pytanie brzmi zatem: czy maszyna wirtualna może nam dostarczyć informacji, których natywna kompilacja nie może uzyskać, co może zwiększyć wydajność naszego kodu?

Sam tego nie widzę. Możemy zastosować większość technik typowej maszyny wirtualnej również do kodu natywnego - chociaż proces jest bardziej zaangażowany. Podobnie możemy zastosować wszelkie optymalizacje natywnego kompilatora z powrotem do maszyny wirtualnej, która korzysta z kompilacji AOT lub optymalizacji adaptacyjnych. Rzeczywistość jest taka, że ​​różnica między rodzimym kodem uruchomionym a maszyną wirtualną nie jest tak duża, jak nam się wydaje. Ostatecznie prowadzą do tego samego rezultatu, ale przyjmują inne podejście, aby się tam dostać. Maszyna wirtualna używa iteracyjnego podejścia do tworzenia zoptymalizowanego kodu, w którym natywny kompilator oczekuje go od samego początku (i można go ulepszyć za pomocą iteracyjnego podejścia).

Programista C ++ może argumentować, że potrzebuje optymalizacji od samego początku i nie powinien czekać na maszynę wirtualną, aby dowiedzieć się, jak to zrobić, jeśli w ogóle. Jest to prawdopodobnie ważny punkt w naszej obecnej technologii, ponieważ obecny poziom optymalizacji w naszych maszynach wirtualnych jest gorszy od tego, co oferują natywne kompilatory - ale nie zawsze tak będzie, jeśli rozwiązania AOT w naszych maszynach wirtualnych poprawią się itp.


0

Ten artykuł jest streszczeniem zestawu postów na blogu, które próbują porównać szybkość c ++ vs c # oraz problemów, które musisz rozwiązać w obu językach, aby uzyskać kod o wysokiej wydajności. Podsumowanie brzmi: „Twoja biblioteka ma większe znaczenie niż cokolwiek innego, ale jeśli jesteś w c ++, możesz to przezwyciężyć”. lub „nowoczesne języki mają lepsze biblioteki i dzięki temu osiągają szybsze wyniki przy mniejszym wysiłku”, w zależności od filozofii.


0

Myślę, że prawdziwym pytaniem nie jest „co jest szybsze?” ale „który ma najlepszy potencjał do zwiększenia wydajności?”. Patrząc na te warunki, C ++ wyraźnie wygrywa - jest skompilowany do natywnego kodu, nie ma JITtingu, jest niższy poziom abstrakcji itp.

To dalekie od pełnej historii.

Ponieważ C ++ jest kompilowany, wszelkie optymalizacje kompilatora muszą być wykonywane w czasie kompilacji, a optymalizacje kompilatora odpowiednie dla jednej maszyny mogą być całkowicie niepoprawne dla innej. Jest tak również w przypadku, gdy wszelkie globalne optymalizacje kompilatora mogą i będą faworyzować niektóre algorytmy lub wzorce kodowe nad innymi.

Z drugiej strony, program JITted zoptymalizuje się w czasie JIT, więc może wyciągnąć pewne sztuczki, których nie może skompilować program wstępnie skompilowany, i może dokonać bardzo szczegółowych optymalizacji dla komputera, na którym faktycznie działa i kodu, na którym faktycznie działa. Po przejściu przez początkowy narzut JIT może w niektórych przypadkach być szybszy.

W obu przypadkach rozsądna implementacja algorytmu i inne przypadki, że programista nie jest głupi, będą prawdopodobnie znacznie ważniejszymi czynnikami - na przykład, możliwe jest na przykład napisanie całkowicie pozbawionego mózgu kodu ciągowego w C ++, który będzie otoczony przez nawet interpretowany język skryptowy.


3
„Optymalizacje kompilatora, które są odpowiednie dla jednego komputera, mogą być całkowicie niewłaściwe dla innego” Cóż, to nie jest tak naprawdę winą za język Naprawdę krytyczny dla wydajności kod może zostać skompilowany osobno dla każdej maszyny, na której będzie działał, co nie stanowi problemu, jeśli kompilujesz lokalnie ze źródła ( -march=native). - „to niższy poziom abstrakcji” nie jest tak naprawdę prawdą. C ++ używa tak samo abstrakcyjnych poziomów jak Java (lub w rzeczywistości wyższych: programowanie funkcjonalne? Metaprogramowanie szablonu?), Po prostu implementuje abstrakcje mniej „czysto” niż Java.
lewo około

„Naprawdę krytyczny dla wydajności kod może zostać skompilowany osobno dla każdej maszyny, na której będzie działał, co nie stanowi problemu, jeśli kompilujesz lokalnie ze źródła” - to nie udaje się z powodu założenia, że ​​użytkownik końcowy jest również programistą.
Maximus Minimus,

Niekoniecznie użytkownik końcowy, tylko osoba odpowiedzialna za instalację programu. Na komputerach stacjonarnych i urządzeniach mobilnych zwykle jest to użytkownik końcowy, ale nie są to jedyne aplikacje, które z pewnością nie są najbardziej krytyczne pod względem wydajności. I tak naprawdę nie musisz być programistą, aby zbudować program ze źródła, jeśli poprawnie napisał on skrypty budowania, tak jak wszystkie dobre projekty wolnego / otwartego oprogramowania.
lewo wokół około

1
Chociaż teoretycznie tak, JIT może wyciągnąć więcej sztuczek niż statyczny kompilator, w praktyce (przynajmniej dla .NET, ja też nie znam java), tak naprawdę nie robi nic z tego. Niedawno przeprowadziłem kilka dezasemblacji kodu .NET JIT i istnieje wiele rodzajów optymalizacji, takich jak wyciąganie kodu z pętli, eliminacja martwego kodu itp., Których po prostu nie robi .NET JIT. Chciałbym, żeby tak było, ale hej, zespół Windows wewnątrz Microsoftu od lat próbuje zabić .NET, więc nie wstrzymuję oddechu
Orion Edwards

-1

Kompilacja JIT faktycznie ma negatywny wpływ na wydajność. Jeśli zaprojektujesz „idealny” kompilator i „idealny” kompilator JIT, pierwsza opcja zawsze zyska na wydajności.

Zarówno Java, jak i C # są tłumaczone na języki pośrednie, a następnie kompilowane w natywnym kodzie w czasie wykonywania, co zmniejsza wydajność.

Ale teraz różnica nie jest tak oczywista dla C #: Microsoft CLR produkuje inny natywny kod dla różnych procesorów, dzięki czemu kod jest bardziej wydajny dla komputera, na którym działa, co nie zawsze jest wykonywane przez kompilatory C ++.

PS C # jest napisany bardzo skutecznie i nie ma wielu warstw abstrakcji. Nie dotyczy to Javy, która nie jest tak wydajna. Tak więc w tym przypadku, z jego eleganckim CLR, programy C # często wykazują lepszą wydajność niż programy C ++. Aby uzyskać więcej informacji na temat .Net i CLR , zobacz „CLR przez C #” Jeffreya Richtera .


8
Jeśli JIT rzeczywiście miałby negatywny wpływ na wydajność, to na pewno nie zostałby użyty?
Zavior,

2
@Zavior - Nie mogę wymyślić dobrej odpowiedzi na twoje pytanie, ale nie rozumiem, w jaki sposób JIT nie może dodać dodatkowego obciążenia wydajności - JIT to dodatkowy proces do wykonania w czasie wykonywania, który wymaga zasobów, które nie są wydane na wykonanie samego programu, podczas gdy w pełni skompilowany język jest „gotowy do pracy”.
Anonimowy

3
JIT ma pozytywny wpływ na wydajność, a nie negatywny, jeśli umieścisz go w kontekście - kompiluje bajtowy kod do kodu maszynowego przed uruchomieniem. Wyniki można również buforować, co pozwala na szybsze działanie niż równoważny interpretowany kod bajtowy.
Casey Kuball,

3
JIT (a raczej metoda kodu bajtowego) nie jest używana do wydajności, ale dla wygody. Zamiast wstępnego budowania plików binarnych dla każdej platformy (lub wspólnego podzbioru, który jest nieoptymalny dla każdej z nich), kompilujesz tylko w połowie, a kompilator JIT wykonuje resztę. „Pisz raz, wdrażaj w dowolnym miejscu” właśnie dlatego tak się dzieje. Wygodę można uzyskać tylko za pomocą interpretera kodu bajtowego, ale JIT sprawia, że ​​jest szybszy niż nieprzetworzony interpreter (choć niekoniecznie wystarczająco szybki, aby pokonać wstępnie skompilowane rozwiązanie; kompilacja JIT zajmuje dużo czasu, a wynik nie zawsze się składa dla tego).
tdammers

4
@Tdammmers, tak naprawdę jest też komponent wydajnościowy. Zobacz java.sun.com/products/hotspot/whitepaper.html . Optymalizacje mogą obejmować takie elementy, jak dynamiczne dostosowania w celu poprawy przewidywania oddziałów i trafień w pamięci podręcznej, dynamiczne wstawianie, de-wirtualizacja, wyłączanie sprawdzania granic i rozwijanie pętli. Twierdzi się, że w wielu przypadkach mogą one więcej niż pokryć koszt JIT.
Charles E. Grant,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.