Marnotrawstwo zasobów przez maszyny wirtualne

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?

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.

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ść.

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.

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#

0

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

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

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.

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.

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

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

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

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.

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.

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.

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