Czy C++ desktop upadnie?

Czy C++ desktop upadnie?

Wątek przeniesiony 2020-04-16 11:11 z Edukacja przez cerrato.

Wibowit
  • Rejestracja:około 20 lat
  • Ostatnio:około 2 godziny
6

Zrobiłem benchmark od nowa używając programów załączonych do poprzedniego posta. Tym razem powinno być czytelniej.

Wersje VMek i kompilatorów:

Kopiuj
:/tmp/binary-trees$ ~/devel/jdk-shipilev/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC -version
openjdk version "15-testing" 2020-09-15
OpenJDK Runtime Environment (build 15-testing+0-builds.shipilev.net-openjdk-jdk-b1230-20200424)
OpenJDK 64-Bit Server VM (build 15-testing+0-builds.shipilev.net-openjdk-jdk-b1230-20200424, mixed mode, sharing)

:/tmp/binary-trees$ ~/devel/jdk-15/bin/java -version
openjdk version "15-ea" 2020-09-15
OpenJDK Runtime Environment (build 15-ea+20-899)
OpenJDK 64-Bit Server VM (build 15-ea+20-899, mixed mode, sharing)

:/tmp/binary-trees$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

:/tmp/binary-trees$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

Kompilacja:

Kopiuj
:/tmp/binary-trees$ javac -d . binarytrees.java
:/tmp/binary-trees$ /usr/bin/gcc -pipe -Wall -O3 -fomit-frame-pointer -march=native -pthread c-gcc-5.c -o c-gcc-5.run
:/tmp/binary-trees$ /usr/bin/g++ -pipe -O3 -fomit-frame-pointer -march=native cpp-gpp-2.cpp -o cpp-gpp-2.run

Pomiary programu w C (wielowątkowego):

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ./c-gcc-5.run 21
	User time (seconds): 21.04
	System time (seconds): 0.10
	Percent of CPU this job got: 358%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:05.89
	Maximum resident set size (kbytes): 351492
:/tmp/binary-trees$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.3.0 /usr/bin/time -v ./c-gcc-5.run 21
	User time (seconds): 12.53
	System time (seconds): 0.56
	Percent of CPU this job got: 345%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.78
	Maximum resident set size (kbytes): 137332
:/tmp/binary-trees$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 /usr/bin/time -v ./c-gcc-5.run 21
	User time (seconds): 23.48
	System time (seconds): 1.21
	Percent of CPU this job got: 358%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.89
	Maximum resident set size (kbytes): 135480

Pomiary programu w C++ (jednowątkowego):

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ./cpp-gpp-2.run 21
	User time (seconds): 11.87
	System time (seconds): 0.03
	Percent of CPU this job got: 99%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:11.91
	Maximum resident set size (kbytes): 265632
:/tmp/binary-trees$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.3.0 /usr/bin/time -v ./cpp-gpp-2.run 21
	User time (seconds): 9.91
	System time (seconds): 6.88
	Percent of CPU this job got: 99%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:16.80
	Maximum resident set size (kbytes): 137544
:/tmp/binary-trees$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 /usr/bin/time -v ./cpp-gpp-2.run 21
	User time (seconds): 24.46
	System time (seconds): 0.15
	Percent of CPU this job got: 99%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:24.62
	Maximum resident set size (kbytes): 137184

Pomiary JVMów z -Xmx1g:

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx1g -XX:+UseParallelGC binarytrees 21
	User time (seconds): 7.93
	System time (seconds): 0.21
	Percent of CPU this job got: 331%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.45
	Maximum resident set size (kbytes): 619836
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx1g -XX:+UseG1GC binarytrees 21
	User time (seconds): 7.86
	System time (seconds): 0.24
	Percent of CPU this job got: 332%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.43
	Maximum resident set size (kbytes): 891660
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx1g -XX:+UseZGC binarytrees 21
	User time (seconds): 14.73
	System time (seconds): 0.77
	Percent of CPU this job got: 198%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.81
	Maximum resident set size (kbytes): 3001448 (wrong - multipmapping not detected)
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-shipilev/bin/java -Xmx1g -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC binarytrees 21
	User time (seconds): 10.67
	System time (seconds): 0.21
	Percent of CPU this job got: 181%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.00
	Maximum resident set size (kbytes): 742892

Pomiary JVMów z -Xmx500M:

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx500M -XX:+UseParallelGC binarytrees 21
	User time (seconds): 12.19
	System time (seconds): 0.19
	Percent of CPU this job got: 333%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.70
	Maximum resident set size (kbytes): 527492
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx500M -XX:+UseG1GC binarytrees 21
	User time (seconds): 12.28
	System time (seconds): 0.30
	Percent of CPU this job got: 316%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.98
	Maximum resident set size (kbytes): 569560
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx500M -XX:+UseZGC binarytrees 21
	User time (seconds): 23.67
	System time (seconds): 0.36
	Percent of CPU this job got: 167%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:14.38
	Maximum resident set size (kbytes): 1589164 (wrong - multipmapping not detected)
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-shipilev/bin/java -Xmx500M -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC binarytrees 21
	User time (seconds): 16.99
	System time (seconds): 0.24
	Percent of CPU this job got: 140%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:12.25
	Maximum resident set size (kbytes): 510964

Pomiary JVMów z -Xmx300M:

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx300M -XX:+UseParallelGC binarytrees 21
	User time (seconds): 22.27
	System time (seconds): 0.14
	Percent of CPU this job got: 363%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.16
	Maximum resident set size (kbytes): 346464
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx300M -XX:+UseG1GC binarytrees 21
	User time (seconds): 17.72
	System time (seconds): 0.16
	Percent of CPU this job got: 295%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.05
	Maximum resident set size (kbytes): 368000
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx300M -XX:+UseZGC binarytrees 21
	User time (seconds): 41.31
	System time (seconds): 0.31
	Percent of CPU this job got: 202%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:20.51
	Maximum resident set size (kbytes): 968412 (wrong - multipmapping not detected)
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-shipilev/bin/java -Xmx300M -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC binarytrees 21
	User time (seconds): 29.24
	System time (seconds): 0.32
	Percent of CPU this job got: 122%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:24.22
	Maximum resident set size (kbytes): 352668

Pomiary EpsilonGC (a.k.a. NoGC):

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx13g -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
[0.001s][warning][gc] Consider setting -Xms equal to -Xmx to avoid resizing hiccups
[0.001s][warning][gc] Consider enabling -XX:+AlwaysPreTouch to avoid memory commit hiccups
Terminating due to java.lang.OutOfMemoryError: Java heap space
	User time (seconds): 4.13
	System time (seconds): 3.17
	Percent of CPU this job got: 300%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.43
	Maximum resident set size (kbytes): 13657388
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx14g -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
	User time (seconds): 4.54
	System time (seconds): 3.15
	Percent of CPU this job got: 274%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.80
	Maximum resident set size (kbytes): 14426340
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xms14g -Xmx14g -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
	User time (seconds): 4.41
	System time (seconds): 3.30
	Percent of CPU this job got: 273%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.82
	Maximum resident set size (kbytes): 14426412
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xms14g -Xmx14g -XX:+AlwaysPreTouch -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
	User time (seconds): 7.88
	System time (seconds): 2.84
	Percent of CPU this job got: 177%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.03
	Maximum resident set size (kbytes): 14727288
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx999g -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
	User time (seconds): 5.04
	System time (seconds): 4.25
	Percent of CPU this job got: 273%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.40
	Maximum resident set size (kbytes): 19219100
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx9999g -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC binarytrees 21
	User time (seconds): 4.82
	System time (seconds): 4.47
	Percent of CPU this job got: 271%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.43
	Maximum resident set size (kbytes): 19219084

Liczba uruchomień GC w trakcie trwania programu:

Kopiuj
:/tmp/binary-trees$ /usr/bin/time -v ~/devel/jdk-15/bin/java -Xmx300M -XX:+UseParallelGC -Xlog:gc:gc.log binarytrees 21
	User time (seconds): 22.04
	System time (seconds): 0.19
	Percent of CPU this job got: 314%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.07
	Maximum resident set size (kbytes): 355800
:/tmp/binary-trees$ grep "Pause Young" gc.log | wc -l
264
:/tmp/binary-trees$ grep "Pause Full" gc.log | wc -l
21

Czyli wykazałeś to co pisałem nas początku, że alokacja Javy jest co najwyżej tak samo wydajna jak malloc, przy założeniu że mamy duży nadmiar wolnej sterty.

Przy dużym zapasie to jest nawet szybsza.

porównywałeś na domyślnym alokatorze z glibc. A można było użyć jemalloc albo tcmalloc, które uchodzą za szybsze

Dodałem je do porównania. Każdy alokator ma jak widać jakieś słabe strony. Domyślny zużywa dużo pamięci. jemalloc i tcmalloc działają wolniej przy programie jednowątkowym. jemalloc jest dużo wolniejszy od tcmalloc.

JVM oszczędza dużo czasu na prealokowaniu pamięci z systemu przy starcie. Alokatory natywne natomiast żądają kolejnych stron w miarę rosnącego zużycia. Jeśli ten benchmark jedyne co robi to jednorazowo alokuje dużą liczbę obiektów, to oczywiste jest, że domapowywanie kolejnych stron przyrostowo będzie mieć większy narzut. JVM zna docelową ilość pamięci, programy w C++ nie.

Może i coś tam alokuje, ale przydział fizycznych stron pamięci RAM do procesu odbywa się przecież leniwie, podczas pierwszego dostępu do strony. Dodałem test z EpsilonGC i działa znacznie wolniej z AlwaysPreTouch niż bez tego. Ustawianie -Xms na wartość równą -Xmx nic nie zmieniło. Ta alokacja wstępna to ma raczej wpływ na przestoje w trakcie działania programu, a nie na całkowity czas przetwarzania. Przetestowałem też Epsilona z -Xmx999g i -Xmx9999g. W obu trybach działa tak samo szybko, a zarazem niewiele wolniej niż przy -Xmx14g. Powodem dla którego działa wolniej jest brak możliwości użycia compressed oops (te są dostępne tylko dla stert < 32g) i widać też, że więcej pamięci jest używane.

Testujesz jeden bardzo specyficzny schemat alokacji. Trudno ekstrapolować to na inne przypadki.

Oczywiście. Skoro pamięcią w C++/ Rust zarządza się zupełnie inaczej niż w Javie/ Haskellu/ JSie/ Pythonie/ itd to ogólnie szukanie identycznych programów w obu grupach jest bez sensu. Tutaj jednak porównuję mechanizmy alokacji pamięci, sensowność konstrukcji programu jest ma niższy priorytet. Jeśli będziemy pisać idiomatycznie to będzie porównanie apples to oranges.

GC Javy w ogóle się wyłączył choć raz? ;)

Włączył czy wyłączył? Zliczyłem ile razy odpalił się GC w trakcie działania programu przy ParallelGC i -Xmx300M. Dość dużo razy się uruchomił zarówno young GC jak i full GC.

Natomiast takie porównanie jest oczywiście i tak bez sensu, bo programy w C++ / Rust alokują na stercie znacznie mniej niż JVM, więc nawet jeśli malloc/new byłoby 2x wolniejsze w C++ to i tak C++ ma dużą przewagę. W C++ można pisać programy całkowicie bez użycia sterty. Tam gdzie alokacja zajmuje za dużo czasu, to się robi pulę / arenę. Tak jak w benchmarkach, które celowo wyrzuciłeś.

W Javie mamy escape analysis, które zamienia automatycznie alokację na stercie na alokację na stosie (obecnie rozwój tych technik odbywa się w GraalVMie). Pule czy areny też można zrobić w Javie. Pul i aren to praktycznie nie spotykam, ani w Javie ani w C++ie, więc to jest dla mnie trochę słaby argument. Pule w Javie znalazłem w Netty gdzieś głęboko w bebechach. Benchmarki z pulami i arenami wyrzuciłem, bo po co porównanie apples to oranges? Tak czy siak mikrobenchmarki nie mają ogólnie sensu (bo można je pisać na multum sposobów), ale ja tutaj przynajmniej chciałem porównać wydajność malloc vs tracing GC (a więc jedna rzecz się zmienia, a nie wiele). Pule i areny są zupełnie czymś innym niż malloc czy GC, bo w pulach i arenach dealokacja następuje tylko i wyłącznie hurtowo (dealokacja dotyczy całej puli czy areny naraz). GC natomiast spokojnie może działać sobie przyrostowo.

No i Shenandoah i ZGC jednak przegrały ten benchmark pod względem zajętości pamięci i czasu wykonania.

Te GC są raczej optymalizowane pod duże sterty, liczone w gigabajtach, a nie megabajtach.

No i na koniec bardzo ważna sprawa:
Te wszystkie usprawniania w JVMie będą dostępne nie tylko dla Javy i innych języków kompilujących się do bajtkodu, ale także dla wszystkich języków obsługiwanych przez GraalVMa. Optymalizujemy jedną VMkę, a multum języków przyspiesza. Bardziej się to opłaca niż pisanie osobno VMek/ JITów do Javy, Pythona, PHP, JavaScriptu, itp itd


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit
vpiotr
Punkt za to ze Ci sie chcialo pisac te testy
Wibowit
No trochę z tym zeszło
KR
Też dałem łapkę w górę.
KR
Fajnie wyszło, że w sumie G1 jest całkiem chyba niezłym kompromisem między wydajnością a pauzami.
Wibowit
Dzięki. Łapki widzę w powiadomieniach.
KR
Moderator
  • Rejestracja:około 21 lat
  • Ostatnio:około 12 godzin
  • Postów:2964
1

W Javie mamy escape analysis, które zamienia automatycznie alokację na stercie na alokację na stosie (obecnie rozwój tych technik odbywa się w GraalVMie)

W praktyce jednak nie działa to w tych sytuacjach w których działa dla C/C++, bo większość obiektów gdzieś ucieka. Przykładowo taka sytuacja - alokuję nowy obiekt aby go zwrócić do callera. Wyeliminuje alokację? No raczej nie, chyba że metoda będzie inline. Ale duża metoda nie będzie inline. W C++ po prostu zrobię kopię albo move. I mogę przekazać obiekt przez 40 poziomów nie robiąc żadnej alokacji. Kopiowanie w obrębie stosu jest bardzo szybkie (~20-30 GB/s, gdzie wydajność alokacji G1 na tym samym sprzecie był na poziomie 7GB/s dla dużych obiektów).
Do tego dodajmy NRVO i nie trzeba nawet kopiować.

Gdyby EA naprawdę działała tak jak alokacja na stosie C++ to ten kod, który optymalizowaliśmy w Javie przez wywalenie alokacji ręcznie i powtórne używanie obiektów nie powinien przyspieszyć. A przyspieszył 8x (i nadal był jeszcze 5x wolniejszy od kodu Rust, który udało się napisać bez sterty i bez puli obiektów).

edytowany 2x, ostatnio: Krolik
Wibowit
a próbowałeś pociąć metody na mniejsze i zrobić benchmark pod zwykłym JVMem i pod GraalVMem (CE i EE)?
KR
Metody właśnie były małe i zwracały mnóstwo małych obiektów.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.