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:
:/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:
:/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):
:/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):
:/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
:
:/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
:
:/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
:
:/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):
:/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:
:/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
vpiotr