Marnotrawstwo zasobów przez maszyny wirtualne

1

@ple: a co ma powiedzieć biedny programista C++, który marnuje ogromne ilości prądu na parsowaniu miliardów linii kodu przy każdej zmianie zmianie pliku, bo używane jest archaiczne podejście wklejania pliku nagłówkowego do pliku źródłowego rodem z lat 70? W obu przypadkach problemem nie jest fakt, że czegoś się nie da zrobić, tylko, że jest to cholernie ciężkie do zrobienia od nowa.

Odnośnie profilowania w kółko tego samego kodu: Chińczycy wpadli na coś takiego https://github.com/alibaba/dr[...]ragonwell8-User-Guide#jwarmup

3

Od początku Java czy C# to był kompromis między wydajnością runtime i kosztem wytworzenia software. Przykładowe zastosowanie: 1GB ramu w AWS kosztuje 18$/miesiąc. To tyle, co junior bierze za godzinę. Z tego powodu optymalizacja zużycia pamięci w systemach typu CRUD nie ma najczęściej sensu.

Ostatnio maszyny wirtualne zrobiły się na tyle dobre, że zapominamy, że one są wolniejsze by-design.
Np problem inicjalizacji o którym piszesz, to konsekwencja kompilowania do bytecode. Dzięki temu rozwiązaniu programiści nawet nie mają jak marnować czasu na optymalizację pod konkretny sprzęt.

Jeśli piszesz programy, w których inicjalizacja, zużycie CPU i RAMu ma znaczenie, to źle wybrałeś narzędzie.

0

Czy zauważyliście jakie to bezsensowne?

Oczywiście, że tak. To jest bezsensowne i wygląda na to, że nawet twórcy platform takich jak .NET i Java też to zauważyli i oferują opcję kompilacji AOT.

W teorii kompilacja w locie może wygenerować lepszy kod niż kompilacja AOT. W praktyce nigdy ta przewaga się nie zmaterializowała i jak zawsze .NET/Java dostawały solidne bęcki w benchmarkach, tak nadal dostają, nawet po 25 latach rozwoju JITów / Hotspotów, ostatnimi czasy nawet większe bo w międzyczasie zmieniła się charakterystyka wydajnościowa pamięci (tzn. powiększyła się różnica między czasem dostępu losowego a sekwencyjnego) itp. A akurat mikrobenchmarki są jednym z takich zadań, w których JIT ma w pewnym sensie łatwiej, bo kodu jest mało i można wykorzystywać pewne specyficzne własności kodu, których zwykle nie da się wykorzystać w dużym programie (np. tylko jedna konkretna implementacja iteratora załadowana przez benchmark prowadząca do trywialnej monomorfizacji; mikrobenchmarki też są zwykle małe więc się w całości inline'ują).

Konkretny przykład, który mieliśmy ostatnio w firmie - odczytujemy spakowaną listę integerów z pliku. Powiedzmy, że plik jest zmapowany pamięciowo, więc I/O nie jest problemem. Ale inty są spakowane "bitowo", np kodowane na 5 bitach aby indeks był mniejszy. Developerzy Lucene robili jakieś cuda na kiju aby zmusić JVM do użycia instrukcji SIMD. Nawet użyli Vector API z JDK 16. I co? Niby wielki suckes - przyspieszyli kod 3x i na moim sprzęcie ich kod uzyskuje zawrotne 3 miliardy intów na sekundę. Nie jest źle, pierwsze wersje dekodowanły 8 razy wolniej, ale pierwsza lepsza libka do pakowania bitowego w C na tym samym sprzęcie dekoduje 24 mld intów na sekundę, czyli jest kolejne 8x szybsza. Co więcej, nawet skompilowana na jakieś archaiczne SSE.2 jest nadal 4x szybsza od kodu JVM, który zna procesor i mógłby wykorzystać (w moim przypadku) nawet AVX 512. W tej sytuacji widać, że znajomość procka w niczym JVMowi nie pomogły. I generalnie tak jest na razie z KAŻDYM przykładem którego miałem okazję się dotknąć.

Dzięki temu rozwiązaniu programiści nawet nie mają jak marnować czasu na optymalizację pod konkretny sprzęt

Nie muszą. Kod zoptymalizowany w C++/Rust pod stare Pentium jest zwykle i tak szybszy niż kod wygenerowany przez HotSpot i zoptymalizowany pod Skylake Xeon. Głównie dlatego, że miażdząca większość optymalizacji działa na każdy procesor, a z kolei nowe procesory projektuje się pod wydajne wykonywanie również starego kodu (wiele nowoczesnych optymalizacji sprzętowych działa na starszym kodzie, i do tego zmniejsza rolę kompilatora - np. automatyczna predykcja skoków - nie ma większego znaczenia jak kompilator wygenerował skoki i czy dał jakieś hinty - nowoczesny procek sobie poradzi). No i zoptymalizowanie kodu pod konkretny procek w C++ to zwykle dodanie jednej flagi do argumentów kompilatora. To jest jakiś wielki problem?

@ple: a co ma powiedzieć biedny programista C++, który marnuje ogromne ilości prądu na parsowaniu miliardów linii kodu przy każdej zmianie zmianie pliku, bo używane jest archaiczne podejście wklejania pliku nagłówkowego do pliku źródłowego rodem z lat 70?

Po pierwsze C++ to już nie jest w tej kwestii state-of-the-art. Istnieją nowsze języki kompilowane natywnie, które nie mają tej przypadłości - np. Rust, D, Zig, Go.
Po drugie to nadal znacznie mniej rekompilacji niż rekompilacja przy każdym uruchomieniu przez użytkownika.

Przykładowe zastosowanie: 1GB ramu w AWS kosztuje 18$/miesiąc. To tyle, co junior bierze za godzinę. Z tego powodu optymalizacja zużycia pamięci w systemach typu CRUD nie ma najczęściej sensu

Pomnóż przez 10 tys klientów i się robi $180k / miesiąc, zakładając że klientom wystarczy po jednym wirtualnym serwerze (a z całą pewnością nie wystarczy jeśli napisano to w jakimś zbloatowanym frameworku na Javie/Pythonie itp). I z tego można już sfinansować całkiem ładną wypłatę dla całego zespołu seniorów i zostanie jeszcze na nowe BMW dla prezesa. Optymalizacja nie opłaca się jeśli działasz na bardzo małą skalę albo nie masz konkurencji. Jak masz konkurencję, to klienci przejdą do takiej, gdzie zamiast płacić $30 / miesiąc, zapłacą $10 / miesiąc.

0
Krolik napisał(a):

@ple: a co ma powiedzieć biedny programista C++, który marnuje ogromne ilości prądu na parsowaniu miliardów linii kodu przy każdej zmianie zmianie pliku, bo używane jest archaiczne podejście wklejania pliku nagłówkowego do pliku źródłowego rodem z lat 70?

Po pierwsze C++ to już nie jest w tej kwestii state-of-the-art. Istnieją nowsze języki kompilowane natywnie, które nie mają tej przypadłości - np. Rust, D, Zig, Go.
Po drugie to nadal znacznie mniej rekompilacji niż rekompilacja przy każdym uruchomieniu przez użytkownika.

To że język ma kompilator natywny nie znaczy jeszcze że skompilowany natywnie kod jest szybki :D Np Go i Swift według Benchmarks Game są wolniejsze od Javy i Haskella. Swift to nawet od Node.js jest wolniejszy :/

Wiem że akurat mowa o zasobach, a nie o szybkości, ale takich porównań co do RAMu nie znam :(

0

@KamilAdam: No oczywiście że kiepsko skompilowany kod może nie być szybki. Podobnie słaby interpreter (np. Python, Ruby) też nie będzie szybki. Dlatego nie należy brać słabych kompilatorów / interpreterów do porównań i nie należy wyciągać z tego wniosków.

Kompilator AOT ma olbrzymią przewagę nad maszyną wirtualną w postaci posiadania znacznie większej ilości czasu i zasobów na optymalizację kodu. Może wykonywać nawet tzw. whole-program optimisation, na co JITy nie mają budżetu (HotSpot kompiluje i optymalizuje zawsze lokalnie jedną metodę na raz). Poza tym JIT sam w sobie musi oszczędzać zasoby podczas kompilacji - nie może sobie zabrać kilku GB pamięci tymczasowej na kompilację kodu; natomiast AOT może, więc może stosować dużo droższe algorytmy. Ta walka jest nierówna już na starcie.

W drugą stronę ktoś powie, że JIT może profilować przed kompilacją i że to daje mu pewną przewagę. No cóż, po pierwsze AOT też może, tylko wymaga to trochę więcej pracy od programisty, po drugie w praktyce zyski z PGO jakie widziałem rzadko przekraczały 5%, więc niemal nikt się w to nie bawi.

0

@Krolik:

i do tego zmniejsza rolę kompilatora - np. automatyczna predykcja skoków - nie ma większego znaczenia jak kompilator wygenerował skoki i czy dał jakieś hinty - nowoczesny procek sobie poradzi). No i zoptymalizowanie kodu pod konkretny procek w C++ to zwykle dodanie jednej flagi do argumentów kompilatora. To jest jakiś wielki problem?

czyli zatem za kilka lat będzie się wyrzucać z codebase kernela likely i unlikely?

2
Krolik napisał(a):

W drugą stronę ktoś powie, że JIT może profilować przed kompilacją i że to daje mu pewną przewagę. No cóż, po pierwsze AOT też może, tylko wymaga to trochę więcej pracy od programisty, po drugie w praktyce zyski z PGO jakie widziałem rzadko przekraczały 5%, więc niemal nikt się w to nie bawi.

Zależy jakiego typu kod podlega optymalizacji. Znasz jakiś AOT do JavaScriptu, który osiąga rozsądną wydajność? C, C++, Rust, etc są zaprojektowane i wykorzystywane tak, by profilowanie nie było mocno potrzebne. Zarówno C++ jak i Rust chwalą się "zero-cost abstractions", czyli konstrukcjami składniowymi, które wyglądają dość wysokopoziomowo, ale są skutecznie i konsekwentnie automatycznie optymalizowane przez kompilator.

0

Tu pełna zgoda. jeżeli język nie został zaprojektowany pod kompilację statyczną, to potem bardzo ciężko go dobrze skompilować statycznie.

1

Developerzy Lucene robili jakieś cuda na kiju aby zmusić JVM do użycia instrukcji SIMD. Nawet użyli Vector API z JDK 16. I co? Niby wielki suckes - przyspieszyli kod 3x i na moim sprzęcie ich kod uzyskuje zawrotne 3 miliardy intów na sekundę. Nie jest źle, pierwsze wersje dekodowanły 8 razy wolniej, ale pierwsza lepsza libka do pakowania bitowego w C na tym samym sprzęcie dekoduje 24 mld intów na sekundę, czyli jest kolejne 8x szybsza.

No to powinni wywoływać skompilowaną natywnie bibliotekę która jest w stanie zrobić to szybciej ;). Np. OpenSSL zawiera różne implementacje funkcji pisane w asemblerze, tylko dlatego, że kompilatory C nie potrafiły wygenerować takiego szybkiego kodu, jak możnaby napisać ręcznie w asm.

Ogólnie to powolny startup JVM jest znanym problem i dla programistów JVM, dlatego co jakiś czas starają się coś na ten temat zdziałać. O GraalVM już było w tym wątku pisane, więc go pominę, ale jednym z innych eksperymentów nad którymi pracują to może być np. Coordinated Restore at Checkpoint (CRaC), w którym chodzi o możliwość snapshotowania stanu JVM i ładowaniu go ponownie w razie potrzeby. Czyli będzie można np. zrobić snapshot JVM niedługo po załadowaniu biblioteki standardowej, dzięki temu kolejne ładowanie już załaduje snapshot, zamiast inicjować JVM ponownie (= szybszy startup).

Inny eksperyment to Project Leyden który miałby wprowadzić do JVM możliwość tworzenia statycznych obrazów aplikacji na podobny wzór jak robi to GraalVM, ale bez kompilacji do kodu natywnego. Miałoby to swoje plusy takie jak np. możliwość uruchomienia aplikacji na innej architekturze (np. arm64), przy czym wprowadzałoby też szereg ograniczeń, no ale wiadomo, nie ma rzeczy idealnych ;).

Podejście dystrybuowania kodu przy pomocy kodu pośredniego wydaje mi się zdecydowanie lepsze od dystrybucji kodu natywnego z tego powodu, że kod pośredni zawsze można skonwertować na natywny w teoretycznie dowolnym momencie. To prawda, że obecnie kompilatory kodu natywnego (GCC, LLVM) jedzą optymalizację HotSpot'a na śniadanie, ale raz wygenerowany kod natywny dla danej architektury już taki zostanie na zawsze i nic z tym nie zrobimy (pomijając uruchomienie go pod emulatorem, albo binarnym translatorem, ale wtedy będzie mu daleko do szybkiego działania). Sposób działania JVM/CLR też nie jest jedynym możliwym sposobem uruchamiania kodu pośredniego, bo np. Apple po wprowadzeniu Bitcode (który jest pochodną LLVM IR) rekompiluje program od razu po uploadzie do AppStore, więc wysyłając jedną binarkę która ma zembedowany kod Bitcode jesteśmy w stanie ściągnąć wiele binarek, które serwery Apple'a wygenerują sobie automatycznie, na różne architektury, czyli taki AOT, ale robiony przez dostawcę oprogramowania, a nie dewelopera.

1

@Krolik:

i do tego zmniejsza rolę kompilatora - np. automatyczna predykcja skoków - nie ma większego znaczenia jak kompilator wygenerował skoki i czy dał jakieś hinty - nowoczesny procek sobie poradzi).

ja chce jakieś source i faktycznie jakiś test przeprowadzony, a nie broszurę Intela

bo brzmi to zbyt fantastycznie, a w rzeczywistości jaki jest trade off? kolejny side channel? :P

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