Wydajność Javy vs wydajność C++

0

W internecie można znaleźć benchmarki na których raz C++ wychodzi lepiej raz Java. Zwolennicy C++ twierdzą, że kod natywny zawsze będzie szybszy od kodu wykonywanego na VM, zwolennicy Javy argumentują, że VM może dokonać optymalizacji jakich nie da się wykonać przy natywnym kodzie.
Ja się pytam kto ma rację? Czy optymalizację są na prawdę aż tak znaczące jak twierdzą zwolennicy Javy czy może kod natywny nie jest znowu taki szybki w porównaniu z kodem wykonywanym na VM?

0

Kod natywny jest szybszy i raczej to się nigdy nie zmieni, jednak mogą się zmienić różnice do takiego stopnia, że dla użytkownika końcowego nie będzie większej różnicy o ile będzie to aplikacja mający być wygodna (jakieś GUI, interface do jakiegoś serwera), a nie coś co działa błyskawicznie i nie zużywa za dużo pamięci (jak bazy danych czy serwery).

0

Zaletą wirtualnych maszyn jest możliwość profilowania w locie, np w funkcji mamy piętnaście ifów, gdzie w pierwszych ośmiu zwykle warunek jest true, a w pozostałych false. Wtedy maszyna optymalizuje kod, daje jakieś hinty dla procesora. W innym wywołaniu programu czy funkcji znowu warunki w ifach mogą mieć inny wzorzec. Inna zaleta to np możliwość wykorzystania najnowszych instrukcji np SSE12345 w x86, NEON w ARM, AltiVec w PPC, itd bez rekompilacji bajtkodu.

Nie ma też żadnego powodu, dla którego kod zarządzany miałby być chociażby odrobinę wolniejszy od kodu natywnego, pomijając narzut na kompilowanie bajtkodu do kodu natywnego i jest optymalizację, także w locie. Kompilatory C++ są dużo dojrzalsze niż JVM czy CLR, przeszły już kilka generacji, przetestowano już najwymyślniejsze metody optymalizacji. W dziedzinie kodu zarządzanego pole wykonano generalnie mniej pracy, a pole do popisu jest przecież większe (patrz: profilowanie w locie).

Podsumowując kod zarządzany wykonuje się teraz relatywnie znacząco wolniej niż natywny z powodów:

  • kompilacji i optymalizacji w locie za każdym razem - przydałaby się jakaś pamięć podręczna na wyniki optymalizacji,
  • dużo większe zapotrzebowanie na pamięć - tego raczej się nie przeskoczy, a częste wywoływanie odśmiecacza jednak ma spory wpływ na wydajność i lagi,
  • niedojrzałości kompilatorów kodu zarządzanego,
  • hostem dla maszyny wirtualnej jest zwykle system napisany w kodzie natywnym, a to jest zwykle barierą - przeskok wywołań z maszyny wirtualnej do bibliotek napisanych w kodzie natywnym wymaga jakichś konwersji danych, specjalnego ich traktowania, mechanizmów obsługi błędów, itd

Prawda jest generalnie taka: Im wyższa abstrakcja nad sprzętem, tym lepsze optymalizacje może wykonać kompilator, ale te optymalizacje będą dużo bardziej skomplikowane, przez co ich implementacja będzie wymagać więcej czasu. Z drugiej strony abstah*jąc od sprzętu jak tylko się da, piszemy dużo szybciej, a nasz kod zwiększa swoją prędkość wraz z pojawianiem się kolejnych, coraz sprytniejszych, optymalizacji w maszynie wirtualnej. Zdecydowanie maszyny wirtualne mają przyszłość i będą zwiększać swoją pozycję na rynku.

0

Kod VM jest potencjalnie szybszy niż kod natywny - bo można go podczas uruchamiania (tzn kompilator JIT może) dostosować do maszyny klienta i warunków. No chyba że jest interpretowany, wtedy kod natywny będzie zawsze znacznie szybszy. Nie widziałem jeszcze żeby VM działała szybciej niż kod w C++ ale to się może zmienić. Za to narzut związany z uruchomieniem kodu VM raczej się nie zmieni u tu natywność zawsze będzie wygrywać (ale i z tym można walczyć - patrz C# i zapisywanie raz skompilowanych zestawów)

edit: o, Wibowit mnie już wyprzedził i w dodatku wyraził to co ja chciałem znacznie jaśniej :).

0

Najpierw należy wziąć pod uwagę, że VM musi budować abstrakcję pomiędzy działającym kodem i systemem operacyjnym, szczególnie jeżeli o alokację pamieci chodzi - GC. VM w takich warunkach nawet z najlepszym generatorem kodu może mieć minimalnie mniejszą wydajność niż typowo natywny kod. Z drugiej strony systemy oparte o wirtualizację (jak Singularity/Midori) mają cholerną wydajność, ogromny zysk wydajności dzięki rezygnacji z masy zbędnych w wypadku VM abstrakcji i zabezpieczeń.

C++ i tak jest wolny, żeby otrzymać coś o sensownej wydajności wypadałoby korzystać wyłącznie z pewnego podzbioru C99 i implementować rozwiązania na konkretne okazje. Wydajność nie jest najważniejsza, bo wtedy wypadałoby zrezygnować nawet ze sprzętowych mechanizmów zabezpieczeń, bo nie wszystkie mają zerowy koszt (tj. sprawdzenia lecą równolegle z translacją adresów czy innym podobnym zabiegiem). Jeżeli bierzemy pod uwagę bezpieczeństwo to VM są bezkonkurencyjne, warto te kilka procent wydajności poświęcić.

0

Ale czy wydajność kodu ma w ogóle jakiekolwiek znaczenie z punktu widzenia całego systemu?
Co z tego, że np. if z C++ będzie szybszy od ifa z Javy, skoro architektura będzie skopana, a baza danych katowana tysiącami niepotrzebnych zapytań?

0

C++ da się skonstruować architektury o takiej samej jakości jak w językach bardziej ludzkich, tyle że wyższym kosztem. Trzeba zbalansować koszt programistów i koszt maszyn.

Wg mnie C++ zwykle przegrywa w dzisiejszych czasach, oczywiście z wyjątkiem programowania pod konkretne urządzenia, np systemy na komórki, gry na konsole, itp

0

A co z obliczeniami naukowymi, data miningiem, programowaniu na klastry? Czy Java nie będzie za wolna do takich zastosowań?

0

Dzięki skopanym genericsom w Javie oraz kulawemu pomysłowi boxingu prymitywów w bajtkodzie, kolekcje (ale nie tablice) podstawowych typów danych w Javie (np Character, Byte, Integer, Float) są dużo wolniejsze i zabierają dużo więcej pamięci.

Tu masz jakieś benche:
http://shootout.alioth.debian.org/u64q/java.php

0

w funkcji mamy piętnaście ifów, gdzie w pierwszych ośmiu zwykle warunek jest true, a w pozostałych false. Wtedy maszyna optymalizuje kod, daje jakieś hinty dla
W większości takich przypadków kompilator może równie dobrze zoptymalizować co maszyna wirtualna. Wszystko zależy od implementacji: można napisać kompilator C++ generujący bardzo powolny kod, jak i bardzo szybką VM Javy.

Poza tym takie ify to optymalizuje procesor w czasie wykonania – mechanizm predykcji skoków.

0

Nie całkiem, istnieją dwa prefiksy podpowiadające najczęstszy kierunek skoku warunkowego, do tego obowiązuje zasada, że skok do tyłu powinien się wykonać, do przodu najczęściej nie. Także przy generacji buduje się odpowiednio kod i ew. stosuje hinty na bazie wyników analizy profilera (chociaż większość programistów ma blade pojęcie co to w ogóle jest profiler...). VM ma ten plus, że może podczas wykonywania przebudować kod, jeżeli stwierdzi, że najczęstszy flow jest inny niż optymalny dla obecnego kodu...

// ja na przykład nie wiem, co to jest ten "profifer" ;] - Ł
// tja, raz napisałem dobrze, raz zrobiłem literówkę i już się czepiasz, do tego strasznie końserwatywnie, edytując post, Adam będzie płakał, że komentarzy nie używamy :P - d
// sztywniak. komentarz do tego postu usiłowałem dodać kilkukrotnie i w końcu mi się znudziło, a przecież nie mogłem nie dosrać, no nie? - Ł
// a tam sztywniak, po prostu poważny jestem, jak na poważnego pracownika przystało - d

0

Słyszałem teorie że potencjalnie szybsze (w teorii) są języki funkcyjne i deklaratywne od imperatywnych - bo kompilator/optymalizator/wtf ma znacznie większe pojęcie o tym co programista chciał wyrazić i może wygenerować czystszy kod. Jakoś tego nie widać po kompilatorach, ale coś w tym jest ;).

deus napisał(a)

Nie całkiem, (...)

A dynamiczna predykcja? Tzn. dwubitowe pola w pamięci procesora i "algorytm" (00 -> 01 -> 11) (11 -> 10 -> 00)

0

Hinty dla procka (mogą być prefiksy czy inne dziwactwa) mają tą wadę, że są statyczne. A ja mówiłem o dynamicznym profilowaniu. Mechanizmy predykcji w procesorze mają małą pamięć o skokach, a poza tym kompilator może zrobić w pewnych przypadkach np zamiast 18 zagnieżdżonych czy sekwencyjnych ifów dodatkowo jednego ifa z warunkiem łączącym tamte 18 w taki sposób, że gałąź bez dalszych ifów wykonuje się bardzo często. Mechanizmy sprzętowe (dynamiczna predykcja, wykonywanie spekulacyjne, wykrywanie zależności między rozkazami, kolejkowanie odczytów i zapisów, itp) są kosztowne energetycznie, zwiększają koszt zaprojektowania procesora i zabierają też znaczną część rdzenia.

Do optymalizacji dynamicznych dochodzi też dynamiczne inlineowanie funkcji. W przypadku wykonywania czystych funkcji (bez efektów ubocznych) w pętli można np alokować obiekt na stosie zamiast na stercie, alokować i inicjować tylko tą częśc obiektu, która jest używana w wywoływanej funkcji - oczywiście mówię o funkcjach przekazywanych poprzez parametr w funkcji wywołującej.

Dynamiczna kompilacja kodu w ogólności otwiera dużo więcej możliwości optymalizacji, a żadnych nie wyklucza.

0

Predykcja predykcją, podstawowe założenia i hinting swoją drogą - nie powinno się liczyć wyłącznie na 'zaawansowane algorytmy' zachwalane przez Intela/AMD - jakbym się nigdy na sztucznej ćwierćinteligencji procesora nie przejechał. W praktyce wszystko powinno być maksymalnie jasno skonstruowane, z ręcznym zarządzaniem cachem itd. jeżeli chce się osiągnąć maksymalną wydajność.

Co do deklaratywności to tak jest w teorii, w praktyce np. GHC - kompilator Haskella - nawet podparty LLVM generuje paskudny kod, jakość kodu generowanego przez MCC - kompilator Curry - podobno jest już zbliżona do C, ale nie miałem okazji przetestować, opieram się na opiniach developerów. W szczególnych przypadkach połączenie silnej optymalizacji kodu funkcyjnego i leniwej ewaluacji potrafi dać bardzo przyjemne efekty - paskudny kod z GHC przebija wydajnością typową implementację algorytmu w C/C++ - ale to rzadkie przypadki. Swego czasu podczas jakiegoś konkursu 'security' napisałem sobie czysto funkcyjnego bruteforce'a do czegoś 'numerycznego', liczenie jakiegoś niby-hasha czy coś w tym stylu (oryginał był w PHP), kod z GHC 6.8 (teraz mamy 6.12) był ledwie 2.5-3x wolniejszy od zoptymalizowanej wersji w C++, jaką potem dla porównania napisałem. Mimo wszystko różnica wyszła na plus - napisanie 6 linii a 36 to jednak spory zysk czasu.

O ile pamiętam to niezłe rzeczy potrafił wyczyniać bodaj Allegro Common Lisp. ale to dosyć stare dzieje. Tutaj powinien wypowiedzieć się Wibowit jak kompilator Scali sobie radzi, nie śledzę jego rozwoju zbyt dokładnie.

0

Akurat kompilator Scali polega w dużej mierze na optymalizacjach przeprowadzanych przez HotSpot czy podobne. Poza tym tworzy różnego rodzaju obejścia na niedostatki Javy jak np specjalizacje (adnotacja @specialized), służące do automatycznego generowania wariantów funkcji generycznych dla typów prymitywnych. Chociaż są i wtyczki do kompilatora Scali, które statycznie inlineują niektóre językowe funkcjonalności.

0

Nie całkiem, istnieją dwa prefiksy podpowiadające najczęstszy kierunek skoku warunkowego, do tego obowiązuje zasada, że skok do tyłu powinien się wykonać, do przodu najczęściej nie.
Które to prefiksy procesor „ma prawo” spokojnie olać. Trzebaby to zmierzyć, ale stawiam że na dzisiejszych procesorach różnica w działaniu kodu z hintami i bez będzie praktycznie żadna.

VM ma ten plus, że może podczas wykonywania przebudować kod, jeżeli stwierdzi, że najczęstszy flow jest inny niż optymalny dla obecnego kodu...
Sprawdzanie softwarowo czy „się opłaca” zmienić kod czy nie na pewno będzie wolniejsze niż zostawienie kodu jak jest.

W praktyce wszystko powinno być maksymalnie jasno skonstruowane, z ręcznym zarządzaniem cachem itd. jeżeli chce się osiągnąć maksymalną wydajność.
W ten sposób piszesz kod pod konkretny procesor, aż ci wyjdzie nowa rodzina procesorów i twoje mozolne ręczne optymalizacje okazują się chybione lub w najlepszym przypadku zbędne…

0
Azarien napisał(a)

W ten sposób piszesz kod pod konkretny procesor, aż ci wyjdzie nowa rodzina procesorów i twoje mozolne ręczne optymalizacje okazują się chybione lub w najlepszym przypadku zbędne…

O, popatrz, a VM może takie rzeczy generować w runtime pod procesor posiadany przez użytkownika...

0

"Intel C++ robi to natywnie – niestety faworyzując procki intela kompletnie olewając optymalizację dla AMD."
Wystarczy wyNOPować sprawdzanie vendora (GenuineIntel, AuthenticAMD) i już dobierane są dobre ścieżki wykonywania :)

Niestety "natywny" kompilator raczej nie doda do kodu instrukcji, których jeszcze nawet nie wymyślono, nie mówiąc o tym, że nie są też powszechne kompilatory, które tworzą kod na platformy, które jeszcze nie powstały.

"Sprawdzanie softwarowo czy „się opłaca” zmienić kod czy nie na pewno będzie wolniejsze niż zostawienie kodu jak jest."
Im sprytniejsza maszyna wirtualna, tym mniej zbędnej pracy jest wykonywane. Dynamiczna dewirtualizacja to jest coś, czego statyczny kod nigdy nie będzie posiadał, a co jest coraz bardziej potrzebne, jeśli chce się pisać zwięzły kod.

Sporo instrukcji x86 jest zaimplementowanych jako mikrokod w procesorze i jest on chyba wymienialny. Dotyczy to instrukcji, które długo się wykonują (chyba > 3 takty). Maszyna wirtualna ma więc możliwość podmiany go w locie. Dzięki VM możliwe byłyby procesory z dynamiczną listą instrukcji, np zajmujemy się szyfrowaniem to VM tworzy instrukcje do wydajnego szyfrowania, podobnie np instrukcje do haszowania, liczenia FFT itp itd

Najfajniejsze w VMach jest to, że wiele zabezpieczeń/ sprawdzeń/ tłumaczeń, które teraz są wykonywane sprzętowo przy praktycznie każdej instrukcji, mogą być w większości wyeliminowane albo zredukowane dzięki wysokopoziomowym dynamicznym optymalizacjom - np translacja adresów fizycznych i logicznych może być całkowicie wyeliminowana, ochrona pamięci na poziomie procesora też - można wszystko odpalać na jednym poziomie uprawnień (ring0 w x86), a dzięki informacjom o np rozmiarach tablic, można chronić pamięć bez udziału sprzętu i tagowania stron w pamięci.

EDIT: Poprawione.

0
Wibowit napisał(a)

Najfajniejsze w VMach jest to, że wiele zabezpieczeń/ sprawdzeń/ tłumaczeń, które teraz są wykonywane sprzętowo przy praktycznie każdej instrukcji, mogą być w większości wyeliminowane albo zredukowane dzięki wysokopoziomowym dynamicznym optymalizacjom - np translacja adresów fizycznych i logicznych może być całkowicie wyeliminowana, ochrona pamięci na poziomie procesora też - można wszystko odpalać na jednym poziomie uprawnień (ring3 w x86), a dzięki informacjom o np rozmiarach tablic, można chronić pamięć bez udziału sprzętu i tagowania stron w pamięci.

Nie tyle ring3 co ring0, poza tym segmenty można zredukować praktycznie do zera (typowy flat jak ma to miejsce w em64t), pozbyć się w dużej mierze stronnicowania itd.

1 użytkowników online, w tym zalogowanych: 0, gości: 1