Marnotrawstwo zasobów przez maszyny wirtualne

Marnotrawstwo zasobów przez maszyny wirtualne
PL
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Postów:104
0

Piszę programy w C# i tak sobie myślę, że za każdym razem gdy ktoś uruchamia mój program to optymalizator za każdym razem wykonuję ciągle tą samą pracę związaną z optymalizacją i kompilacją (tych samych fragmentów) do kodu natywnego. Czy zauważyliście jakie to bezsensowne?

edytowany 3x, ostatnio: ple
PR
PR
  • Rejestracja:około 4 lata
  • Ostatnio:prawie 4 lata
  • Postów:204
2

Ale wynik kompilacji i optymalizacji w przypadku C# za każdym razem jest inny co nie jest możliwe, w C++. Dlatego to ma sens.

99xmarcin
  • Rejestracja:prawie 5 lat
  • Ostatnio:4 miesiące
  • Postów:2420
4

Nie wiem jak w .NET Core, ale w Javie optymalizacja następuje dopiero po fazie warmup'u. Czyli przez pewien krótki czas VM zbiera dane i statystyki. Oznacza to że jeżeli program wywołasz z innymi argumentami lub dostanie on na wejście inne dane to mogą zostać zaaplikowane inne optymalizacje.

Niemiej jednak to co zauważyłeś to niezbity fakt, i w wielu aplikacjach jest dokładnie tak jak mówisz. Rodzi to pewne problem np. start i warmup aplikacji - w czasie którego wydajność kuleje. Dlatego w przypadku JVM'a powstał projekt GraalVM/Native a w .NET'cie od dawna mówiło się o ahead-of-time-compilation.

Ma to szczególne znaczenie tam gdzie szybki start aplikacji jest bardzo ważny czyli np. funkcje lambda (chmurowe) lub aplikację GUI.

Z drugiej strony to że operujemy na bytecodzie który jest dopiero potem kompilowany sprawia że ten sam exe'k wykonana się i na standardowym PC i na maszynie z procesorem ARM. Zyskujemy więc dużą przenośność.


Holy sh*t, with every month serenityos.org gets better & better...
edytowany 3x, ostatnio: 99xmarcin
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
5

to optymalizator za każdym razem wykonuję ciągle tą samą pracę związaną z optymalizacją i kompilacją (tych samych fragmentów) do kodu natywnego

Niekoniecznie. Siła JITa polega na tym, że może optymalizować kod w oparciu o informacje z runtime, co sprawia że może zrobić lepsze optymalizacje niż te dostępne ahead-of-time. Prosty przykład do zobrazowania problemu: wyobraź sobie że twój program pobiera liczbę x a następnie wykonuje wiele operacji dzielenia przez tą liczbę. Optymalizacja na poziomie kompilacji niewiele tu może zdziałać, ot wstawi tam jakiegoś div. Ale na etapie runtime, może okazać się że x = 2 i w takim razie te wszystkie dzielenia można załatwić wielokrotnie szybszym przesunięciem bitowym. Nie można zakładać że optymalizacje zawsze będą takie same.
Co więcej, może się okazać że wyjdzie nowa wersja Javy czy .NET na nową generacje CPU które mają lepsze i szybsze instrukcje. W przypadku ahead-of-time musiałbyś kompilować swój program ponownie pod tą nową architekturę podczas gdy w przypadku bajtkodu i JITa nie ma takiego problemu.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 1x, ostatnio: Shalom
KamilAdam
  • Rejestracja:ponad 6 lat
  • Ostatnio:4 dni
  • Lokalizacja:Silesia/Marki
  • Postów:5505
0
ple napisał(a):

Czy zauważyliście jakie to bezsensowne?

Ze wszystkim co napisali przedmówcy się zgadzam jednak w niektórych przypadkach (np. krótko żyjące aplikacje) każdorazowa kompilacja przy starcie faktycznie nie ma sensu. Dlatego Java ma nowy kompilator Graal który jest w stanie stworzyć natywny skompilowany plik wykonywalny. Dziwnie że w M$ nie pracują nad podobnym rozwiązaniem dla C#


Mama called me disappointment, Papa called me fat
Każdego eksperta można zastąpić backendowcem który ma się douczyć po godzinach. Tak zostałem ekspertem AI, Neo4j i Nest.js . Przez mianowanie
edytowany 1x, ostatnio: KamilAdam
obscurity
.net miał kompilację do kodu natywnego gdy java jeszcze nie wiedziała co to lambda
PL
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Postów:104
0

Z tego co wiem to chyba pracują nad .NET native.

Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 4 godziny
2
KamilAdam napisał(a):
ple napisał(a):

Czy zauważyliście jakie to bezsensowne?

Ze wszystkim co napisali przedmówcy się zgadzam jednak w niektórych przypadkach (np. krótko żyjące aplikacje) każdorazowa kompilacja przy starcie faktycznie nie ma sensu. Dlatego Java ma nowy kompilator Graal który jest w stanie stworzyć natywny skompilowany plik wykonywalny. Dziwnie że w M$ nie pracują nad podobnym rozwiązaniem dla C#

  1. Część Graala, która produkuje samodzielne binarki skompilowane AOT (i bez żadnego JITa w środku) to native-image. Cała reszta Graala to JIT i części zbudowane wokół JITa.
  2. .NET ZTCW od dawna jest mieszanką kodu natywnego i JITowanego. Dla przykładu kod biblioteki standardowej jest podobno częściowo skompilowany AOT i dzięki temu programy pod .NETem szybciej startują.

Z tego co wiem to chyba pracują nad .NET native.

Poszukałem na szybko informacji o tym. Wygląda na coś podobnego do GraalVM native-image (podobieństwa to np. wycięty całkowicie JIT czy konieczność ręcznej konfiguracji refleksji w wielu przypadkach). Nie jestem pewien, ale chyba to .NET Native jest ograniczone do UWP (universal Windows platform), wolno rozwijane i mało popularne. Nie udało mi się znaleźć żadnych fajnych przykładów na wykorzystanie .NET Native. Z drugiej strony, native-image z GraalVMa jest wykorzystane ładnie np. w https://quarkus.io/#container-first czy https://docs.micronaut.io/latest/guide/index.html#graal Wygląda też na to, że i w Springu intensywnie pracują nad kompilacją AOT: https://spring.io/blog/2021/03/11/announcing-spring-native-beta

Nawiązując trochę do tego co napisał @Shalom:

JITy wykonują https://en.wikipedia.org/wiki/Automatic_vectorization i dzięki temu są w stanie wykorzystać najnowsze zestawy instrukcji wektorowych (tzn. chodzi o SIMD), które oferuje procesor. Dzięki temu obsługa AVX-512 czy innych najnowszych bajerów pozwala nie marnować krzemu. Problem w tym, że autovectorization działa dość rozczarowująco, tzn. sprawdza się w prostych przypadkach. Autorzy Javy mieli nadzieję na to, że autowektoryzację da się znacząco poprawić, ale niestety nie wyszło. Dlatego stworzono Vector API z Project Panama: https://inside.java/tag/panama Wersje testowe są już w Javie 16 https://openjdk.java.net/jeps/338 czy w nadchodzącej Javie 17. Vector API jest API średniego poziomu - wyżej niż intrinsics odpowiadające 1:1 instrukcjom procesora, ale niżej niż zwięzły i czytelny kod skalarny, bo operujące na wektorach o tej samej wielkości co te w procesorze. Dzięki temu, że Vector API operuje bezpośrednio na wektorach imitujących wektory procesora, to odpada cały proces autowektoryzacji. Zostaje JITowanie kodu do postaci niższego poziomu. Pojedyncza instrukcja z Vector API kompiluje się do jednej lub więcej instrukcji procesora. Im nowszy procesor tym więcej będzie oferował różnych instrukcji, więc prawdopodobnie operacje z Vector API łatwiej będą się mapować na nowsze zestawy instrukcji. Rozmiar wektora w Vector API też jest w pewnym sensie abstrakcyjny (w sensie takim programistycznym), tzn. może się dostosowywać do rozmiarów wektorów obsługiwanych przez procesor na którym chodzi aktualnie JVMka.

Problemem kompilacji AOT jest to, że trzeba wybrać architekturę procesora (i używane zestawy instrukcji) od razu, jeszcze przed przygotowaniem programu do dystrybucji. Z tego wynika, że trudno (lub niemożliwe) jest przygotować natywną binarkę do obsługiwania przyszłych zestawów instrukcji. Vector API rozwiązuje tę sprawę. Kolejną sprawą rozwiązywaną przez Vector API jest bezpieczeństwo. Błędy programistyczne przy korzystaniu z Vector API skutkują podobnymi efektami co błędy programistyczne przy pisaniu kodu skalarnego, tzn. dostajemy exceptiony zamiast segfaultów albo innych sygnałów od OSa zabijających VMkę, dostajemy exceptiony zamiast https://en.wikipedia.org/wiki/Memory_corruption itp itd Dzięki wysokiemu poziomowi bezpieczeństwa (standardowego dla całej platformy Java), można bezpieczne wstawiać Vector API gdziekolwiek chcemy. Jest to zupełnie odmienne od niskopoziomowych intrinsics opartych na konstrukcjach unsafe (tzn powodujących segfaulty, memory corruption, etc zamiast standardowych przewidywalnych wyjątków). ZTCW to np. SSE czy AVX2 instrinsics w .NET są generalnie unsafe (np. używają wskaźników, które są unsafe). Brak bezpieczeństwa SIMD intrinsics w .NET był jednym z głównych powodów odrzucenia zwektoryzowanego algorytmu sortowania: https://github.com/dotnet/runtime/pull/33152


"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
Zobacz pozostałe 20 komentarzy
Wibowit
Trik w tym, że JIT jest w stanie wyeliminować sporo z tych sprawdzeń indeksów, jeśli wywnioskuje, że nie są potrzebne w danym miejscu (tzn zawsze przejdą). Analogicznie do sytuacji z intrinsics ma się sprawa z np. z wczytywaniem danych z tablic. Normalne tablica[indeks] sprawdza indeks, ale można użyć Unsafe.getInt i wymusić pominięcie tego sprawdzenia. Dzięki sprawdzaniu indeksów, null pointerów, itp itd dostajemy normalne exceptiony typu NullPointerException czy IndexOutOfBoundsException zamiast segfaultów i https://en.wikipedia.org/wiki/Memory_corruption
KR
@Wibowit: chodziło mi o to, że takie odpowiedniki Vector API też masz dla C, C++ i Rusta. Masz pełne spektrum - od pisania kodu w sposób "normalny" i liczenia na to, że kompilator zwektoryzuje (co LLVM robi i tak lepiej niż HotSpot), poprzez wysokopioziomowe abstakcje w stylu iteratorów optymalizowanych SIMD (np. biblioteki faster, vector, packed_simd itp) aż do niskopoziomowych intrinsiców. Różnica jest taka, że żeby zaktualizować bibliotekę do nowej wersji AVX nie trzeba czekać na nową wersję całego JVMa tak jak jest w świecie Javy.
Wibowit
Popatrzyłem sobie na https://rust-lang.github.io/packed_simd/packed_simd_2/ i jest prawie dobrze. Rozmiary wektorów są zawsze ustalone na sztywno (np. 256 bitów), nie ma odpowiednika np. https://docs.oracle.com/en/java/javase/16/docs/api/jdk.incubator.vector/jdk/incubator/vector/LongVector.html#SPECIES_PREFERRED który automatycznie dostosowuje się do tego co oferuje procesor. W packed_simd trzeba pisać kod osobno pod każdy rozmiar sprzętowego wektora. Ciekawe jak z wydajnością tego packed_simd. Chyba da się napisać dowolne algorytmy bez użycia unsafe.
Wibowit
Hm, popatrzyłem głębiej i jednak jest coś abstrakcyjnego https://rust-lang.github.io/packed_simd/packed_simd_2/struct.Simd.html czym (w połączeniu z const generics) można by się posłużyć do napisania kodu niezależnego od długości wektora. Czyli całkiem ok. Zobaczymy jak to wyjdzie w praniu. Ten packed_simd też się dynamicznie zmienia: https://github.com/rust-lang/packed_simd/issues/282
KR
pcked_simd jest jeszcze dość niskopoziomowe. Nad tym masz kolejne abstrakcje takie jak faster, gdzie się w ogóle nie przejmujesz długością. Inna sprawa że użycie kodu SIMD to zwykle niewielkie fragmenty są - więc nie jest to jakiś problem dopasować to pod określoną platformę. To jest taki obszar, gdzie właśnie chcesz mieć kod dopasowany idealnie pod sprzęt, a nie przenośny. Tylko wtedy daje to naprawdę duże zyski. Przenośne ma być pozostałe 99%.
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 16 godzin
  • Postów:5107
0

Warto chyba zaznaczyć, że niektóre elementy powoli będzie można przenosić z runtime np. refleksję na moment kompilacji za pomocą source generators.

edytowany 1x, ostatnio: WeiXiao
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 4 godziny
0
WeiXiao napisał(a):

Warto chyba zaznaczyć, że niektóre elementy powoli będzie można przenosić z runtime np. refleksję na moment kompilacji za pomocą source generators.

W Javie rzeczy typu generate-sources to były chyba od starożytności: https://maven.apache.org/guides/mini/guide-generating-sources.html

Nie wiem czy wszyscy się skuszą na to. Jak ktoś jest miłośnikiem refleksji (i/ lub bibliotek wykorzystujących refleksję do podstawowych funkcjonalności) to chyba przy tym zostanie.


"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
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 2 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4706
1

@WeiXiao:

. refleksję na moment kompilacji za pomocą source generators.

To raczej u niemieckich architektów.

Obecnie jednak najlepiej wygląda mechanizm Class Derivation to coś pomiędzy makro, a refleksją. Działa w czasie kompilacji - zaletą jest to, że "nie wszystko przejdzie". Jest w Scali 3 i od dawna w Haskellu.
https://dotty.epfl.ch/docs/reference/contextual/derivation.html
https://downloads.haskell.org/~ghc/7.8.4/docs/html/users_guide/deriving.html


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
KamilAdam
Class Derivation w Javie to by było coś
SL
  • Rejestracja:około 7 lat
  • Ostatnio:minuta
  • Postów:862
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/dragonwell8/wiki/Alibaba-Dragonwell8-User-Guide#jwarmup

edytowany 1x, ostatnio: slsy
PR
pragmaticdev
Programiści C++ piją więcej kawy. Ach te czasy co to się kod 1,5h budował ... https://imgs.xkcd.com/comics/compiling.png
AS
  • Rejestracja:prawie 4 lata
  • Ostatnio:około 15 godzin
  • Postów:344
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.

KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
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.

edytowany 4x, ostatnio: Krolik
KamilAdam
  • Rejestracja:ponad 6 lat
  • Ostatnio:4 dni
  • Lokalizacja:Silesia/Marki
  • Postów:5505
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 :(


Mama called me disappointment, Papa called me fat
Każdego eksperta można zastąpić backendowcem który ma się douczyć po godzinach. Tak zostałem ekspertem AI, Neo4j i Nest.js . Przez mianowanie
edytowany 1x, ostatnio: KamilAdam
KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
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.

edytowany 2x, ostatnio: Krolik
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 16 godzin
  • Postów:5107
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?

edytowany 2x, ostatnio: WeiXiao
KR
Nie sądzę, bo kernel musi działać na różnych rzeczach łącznie z bardzo egzotycznymi architekturami. Natomiast na Intelu / AMD nie ma większego znaczenia, bo branch predictor jest dynamiczny, więc się i tak nauczy skoków. No chyba że dany skok jest tylko raz, poza pętlą, ale wtedy to raczej trudno aby miało to wpływ na wydajność.
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 4 godziny
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.


"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
Azarien
hmm wywołanie funkcji wirtualnej nie jest takie zero-cost, przynajmniej w teorii (optymalizacja może sprawić, że będzie)
Wibowit
nie wszystkie abstrakcje w C++ czy Ruście są "zero-cost", przykładowo właśnie wywołania funkcji wirtualnych są zarówno w C++ (słówko virtual) i Ruście (trait objects). niemniej jednak, masa abstrakcji jest "zero-cost", a funkcje wirtualne w C++ i Ruście używane są stosunkowo rzadko (licząc czas CPU).
KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
0

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

WO
  • Rejestracja:ponad 6 lat
  • Ostatnio:5 miesięcy
  • Postów:10
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.

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 16 godzin
  • Postów:5107
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

edytowany 3x, ostatnio: WeiXiao
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 4 godziny
1

Hinty są raczej ignorowane. Za to zagęszczenie skoków i przepływ sterowania ma znaczenie (chociaż ich procentowy wpływ na wydajność zależy od zawartości reszty kodu).

https://www.agner.org/optimize/microarchitecture.pdf

Branch prediction in Intel Haswell, Broadwell, Skylake, and other Lakes
The measured throughput for jumps and branches varies between one branch per clock
cycle and one branch per two clock cycles for jumps and predicted taken branches.
Predicted not taken branches have an even higher throughput of up to two branches per
clock cycle.
...
Branch prediction in AMD Zen
Branch information is no longer attached to the code cache in Zen 1 and 2, and there is no
serious problem in having many branches in the same code cache line. This does not apply
to the Zen 3 which has lower throughput if there are more than 8 jumps or taken branches in
a 64-bytes block of code.
...
Bottlenecks in Haswell and Broadwell
Bottlenecks in Skylake and other Lakes
Branch prediction
The throughput for taken branches is one jump per clock or one jump per two clocks,
depending on the density of branches. Predicted not taken branches have a higher
throughput of two per clock. Therefore, it is advantageous to organize branches so that they
are most often not taken.
...
AMD Zen 1-2 pipeline
Branches and loops
The branch prediction mechanism is described on page 34. There is no restriction on the
number of branches per 16 bytes of code that can be predicted efficiently.
Jumps generally have a throughput of one taken jump per two clock cycles. This includes
direct jumps, indirect jumps, calls, returns, and taken branches. However, tiny loops with a
maximum of five instructions and no 64-bytes boundary in the loop can execute in a single
clock cycle per iteration. Fused compare and branch instructions count as one here.
Branches that are not taken have a throughput of two not taken branches per clock cycle.
...
AMD Zen 3 pipeline
Branches and loops
The behavior of jumps and branches in Zen 3 is different from the previous Zen designs.
The throughput for jumps and taken branches is now one taken jump per clock where
previous AMD processors had one taken jump per two clocks in most cases. The
throughput for predicted not taken branches is two not taken branches per clock cycle. The
throughput for calls and returns is one call or return per two clocks.
The performance is inferior if there is more than two jump instructions or taken branches in
an aligned 16 bytes block of code. This is similar to the old K10 and earlier AMD
processors. The average throughput is measured to 3 clocks per jump if there are three
jumps or taken branches in an aligned 16-bytes block of code. The delay per jump is
increased to 4 clocks per jump if there are 4 - 6 jumps in 16 bytes of code, and 10 - 12
clocks in case of the maximum density of one jump per 2 bytes of code. These numbers are
approximately the same for unconditional jumps and taken conditional jumps.


"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 3x, ostatnio: Wibowit
Azarien
  • Rejestracja:ponad 21 lat
  • Ostatnio:minuta
0

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

Predykcję skoków stosuje się już od dawna - w x86 od czasu pierwszych Pentiumów.
Ogólna zasada jest bardzo prosta - jeśli w danym miejscu skok się poprzednio odbył (albo odbył się już kilka razy pod rząd), to zakładamy że następnym razem też się odbędzie. I odwrotnie.
W bardziej zaawansowanej postaci procesor może wykrywać powtarzalne wzorce - np. skok następuje co drugi raz, albo co dziesiąty.

Pomyłka kosztuje dodatkowe cykle, ale jeśli częściej trafiamy niż nie to to się opłaca.

edytowany 1x, ostatnio: Azarien
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 16 godzin
  • Postów:5107
0

@Azarien:

to że branch predictor jest od dawna to wiem,

ale mi chodzi o te wszystkie heurystyki i inne cuda które mają nowoczesne procesory.

edytowany 3x, ostatnio: WeiXiao
KR
Dynamiczny branch predictor powoduje, że nie ma większego znaczenia jak kompilator wygeneruje skoki a i tak będzie działać dobrze. A register renaming powoduje, że nie ma aż tak dużego znaczenia jak kompilator przydzieli dane do rejestrów. Ogólnie te heurystyki i cuda powodują, że w sensownie napisanym kodzie spokojnie da się utrzymać IPC > 2, a widziałem też nawet 3-4.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)