Mainstream

tryby rzeczywistości 9/9 #Po bożemu uruchamiamy z bootloadera

//wróć do spisu treści; tekst jest fragmentem większej całości;

#Po bożemu uruchamiamy z bootloadera coś (przyjmijmy, że celem naszym OS).

GRUB lub WBM (zależy czy gramy w pingwina czy w okno) na niskich adresach (pierwszy megabajt) ładuje kernel. W tym momencie istnieje tylko ring0 (pełne prawo do każdej operacji na wszystkim). Jeśli na tym etapie jest dziura, albo ktoś coś zje… to sprawa jest nie do odczarowania. Dlatego kernel OS jest tak istotnym i prześwietlanym elementem. Ponieważ okna nie są otwarte to nie mają tam dość ludzi do przeglądania tego bajzlu i przekroczyli poziom ogarniania tej złożoności (co widać na statystykach utylizacji w zastosowaniach końcowych – win7 zrobił się ostatnio popularny), a reszta próbuje przesiąść się na pingwiny.

W zasadzie w tym momencie już byliśmy (wcześniej omówiony) – włączany jest bit PG w CR0 czyli stronicowanie pamięci. Ten skryba tłumaczący adresy wirtualne na fizyczne. Dochodzi do reorganizacji IDT (przerwań sprzętowych) czyli ładowane są sterowniki.

W ramach pamięci uorganizowanej (stronicowanie) pamięci kernel z uprawnienia ring0 tworzy adresowanie wirtualne (może być rozciągnięte poza fizyczny RAM na dyski czy cokolwiek, z punktu widzenia sterownika sygnał z SSD czy RAM różni się tylko opóźnieniem co jest kwestią do rozwiązania oczekiwaniem). Opis uprawnienia segmentu (kernel/user) są przechowywane w deskryptorach GDT/LTD i pilnuje tego bezpośrednio procek. Ale ze względu na uprawnienia (zmiana ringu przez syscall lub bramkę TG, będzie dalej po TSS) działamy zasadniczo w dwóch trybach: kernel w ring 0 może wszystko (pełny dostęp do pamięci mapowanej fizycznie, i wirtualnie, modyfikacja tablic pamięci/mapowania od root w CR3, i gdyby nie przejściówka OS, która porządkuje tłumaczenie adresów pamięci/urządzeń [pamięć to takie samo urządzenie jak brzęczyk czy mysz – po prostu ma więcej kabli podpiętych] to ma uprawnienia by wbijać adresy fizycznie [DMA] tłumaczone przez MMU [można przez ioremap], ale z grzeczności idzie to przez sterownik i dlatego to mapowanie wykonywane jest przez MMU, choć kernel może sobie wbić bezpośrednio, że nie musi odpalać MMU, co stosuje się w diagnostyce, ale wtedy cała pamięć jest wożona na sztywno, w normalnym OS wykoleją się procesy po takich numerach).

Dlatego kernel musi zajmować część pierwszego rdzenia klocka (zwyczajowo afiliacji zero, ale nie jest to konieczne, model specyfic kernel jak sobie zrobicie może rozproszyć operacje po rdzeniach, tylko trzeba z góry wiedzieć (proces instalacji) których i kiedy, zazwyczaj się tego nie robi do normalnych OS. Spora część poleceń takich jak CLI/STI, LGDT/LIDT, I/O, WRMSR/RDMSR jest wykonywana raczej z rdzenia zero (bootstrap), ale ponieważ dla uproszczenia produkcji (i wcześniej planowego SMP – symetryczna wielowątkowość) wszystkie rdzenie są podpięte do urządzeń. Co tak nie do końca ma sens we wszystkich zastosowaniach, ale byłoby bardzo drogo robić specyficzne klocki, które wymagają specjalnego OS, który na pewno tego nie używa (bo, że domyślnie to nic nie znaczy) i mają spalone w postprocesie wybrane ścieżki. Głownie chodzi o procedury testowania klocków w produkcji – cyrk byłby nieziemski. Choć w praktyce wstawania samego klocka to rdzeń 0 inicjuje APIC i budzi pozostałe przez SIPI.

Dla przykładu scheduler linuxa migruje procesy między rdzeniami co potrafi bulwersować przy przenoszeniu z okna na pingwina, bo trzeba powstrzymać kernel z dala od rdzenia, który chciałeś okupować procesem w całości i na wyłączność. Bulwersujące jest to dlatego, że każdy rdzeń ma własny fetch unit i w nim predyktor instrukcji (BPU) gdzie SRAM zawiera tablicę asocjacyjn zawierającą adres skoku → docelowy adres + historia dla predyktora instrukcji (BTB, TAGE, inne wynalazki) i tch wpisów mieści się tam max16k, ale w większości klocków między 6k do 12k (Skylake, Alder) wystarcza. I jak to zmigrujecie to bez wyników BTB i TAGE więc zgadywanka zaczyna się od nowa. Oczywiście procesy kernela zazwyczaj nie sypią się na losowości, no ale wartością bazową coinflip jest 50:50 więc idzie w piach trochę cykli zanim branch predictor się ogarnie. Chyba, że ktoś dopisał w kodzie hinty. Ale i tak wydajnościowo jest to o kant d i sensownie jest tego przy konstruowaniu kernela zabronić jeśli oczekujemy wydajności. Oczywiście jeśli po każdej migracji chcecie mieć zimny start to sami chcieliście serii cache missów. W ramach rachunków podpowiem: BTB miss to10-20cykli, TAGE cold start to 100-1k cykli, L1miss 4-10cykli, L2miss 10-40, DRAM 100-300, TLB 10-100. Znaczy jest z tego rachunek z prąd. To w tysiące cykli na wieziony wątek schodzi. Przeniesienie kworker, softirq, rcu to wykonanie tych samych instrukcji na nowym rdzeniu ale BTB jeszcze nie wie że spin_lock chodzi w pętli, TAGE nie “pamięta”, że if (p->state == …) to zazwyczaj jest prawda, cache nie ma struktur task_struct, runqueue. Częste migracje to tak 10 do 30% wydajności w dół. Więc na pingwinie robi się na chama CPU affinity taskset, sched_setaffinity, zmienne per cpu per_cpu, this_cpu_*, przerwania IRQ przypisane do rdzenia /proc/irq/X/smp_affinity, balansowanie obciążenia schedulera i już czysto techniczna kwestia NUMA-aware scheduling. W razie wątpliwości wbijacie pingwinowi na basha:

# Zobacz migracje: perf stat -e migrations,context-switches ./program

# Cache missy: perf stat -e L1-dcache-load-misses,L1-icache-load-misses ./program

# BTB missy (na Intelu): perf stat -e branch-instructions,branch-misses ./program

W Oknie jest Ideal Processor i Last Processor (ostatni rdzeń) co ogranicza migrację (balancing), ale nie wyklucza ich całkowicie. Możecie przyjąć, że jak na jakimś afinity dowalicie wysoki prorutet (na przykład władujecie się na niezerowy rdzeń) to windows się dość szybko wyprowadzi ze swoimi pierdołami na inny lub przeciąży zerówkę (OS najlepiej trzymać na zerówce, chyba że robicie jakieś dłubanie po sprzęcie – indeksowanie plików). Tyle że to też jest zależnie od procesora bo Ryzen daje odczyty niekompatybilne z liczydłem w windzie i potrafi wymusić bezcelowe migracje dość często. Najczęściej więc wrzuca się SetProcessAffinityMask i SetThreadAffinityMask żeby się nie kopać z koniem. Ustawianie w pingwinie migration/numerek to dłuższa przeprawa by okiełznać hydrę. Co nie znaczy, że windows nie ma swoich widzimisiów. Jak mu ustawicie power plan na pełną petardę to się postara zmigrować, więc włączycie pełną paterdę, ten zacznie przeprowadzki, wrzucacie program (powiedzmy grę na sześć rdzeni) i zaczynają się przeliczenia, przepychanki i winda pakuje się do domu na stan taki jaki miała przy core parking. To kopanie się z koniem potrafi zająć nawet kilkanaście sekund kiedy jakiś grubszy unreal engine zaczyna się rozpychać po rdzeniach i rozpisywać procesy.

Co zapewne wydaje się dziwne, bo od windy7 właśnie ta migracja miała być zaletą, a w pingiwnie ma niby ustawione na niską częstotliwość z preferencją stay local. Kłopoty zaczynają się gdy złożone, nowoczesne aplikacje rozpychają się po rdzeniach zaburzając OSowi kalkulacje tych parametrów. I nagle użyszkodnicy są zdziwieni, że rdzeń gry, który potrzebuje niskiej latency z I/O musi pytać o wyniki z drugiego rdzenia obsługującego myszę i klawiaturę. No to wyobraźcie sobie taką bekę przy podłączeniu sterowania w urządzeniu liczącym prędkość w grubych setkam metrów na sekundę kiedy wbija cross-core latency po ISR (Interrupt Service Routine). Przy 4GHz to potrafi być blisko milisekundy (ca 0.8) czyli tak 1/20 klatki przy 60fps. Tyle że to jest najdelikatniejszy wariant problemu, ponieważ w systemach kontroli ognia droga od kamery do wyświetlacza i do decydenta łącznie daje na tyle duże opóźnienie, że nie bez powodu się skarżą i w zasadzie trzeba część apki upilnować z tym, aby zarówno urządzenie wejścia (kamera) i sterowanie (z wyzwalaczem ognia) było pod jednym rdzeniem i sobie nigdzie nie poszło w migracje. Dodatkowo sterowniki w urządzeniach kontrolnych zazwyczaj mają odświeżanie 1MHz więc doliczamy milisekundę oczekiwania na pakiet.

Naturalnie dla mnie to żaden problem, bo moja wybitna percepcja świetnie sobie rodzi z grami turowymi. Ale ludzie używający sprzętu do czegoś tam mają jakąś inną konstrukcję okablowania oko-ręka i ich potrafi taki lag drażnić bo nie mogą w coś tam trafić. W treningu potrafią to kompensować, ale dalej czują dysonans. Jak się z nimi trochę dłużej popracuje to największa beka jest przy najcięższym sprzęcie kiedy trzeba tłumaczyć, że ruch czy obrót kilku ton serwami ma lag i naprawdę nic na styku zasilani/serwo/dużo ton nic się nie da zrobić bo fizyka jest jaka jest.

Użyszkodnik (ring3) może wbijać wyłącznie przez MMU, a jego procesy z fork/createprocess są grzecznymi pytaniami do kernela, czy ten łaskawie by zechciał i udostępnił. Ze względu na deskryptor GDT/LTD w miarę nowy klocek nie wykona czegoś podpisanego niewłaściwie względem źródła zapytania. Dlatego użyszkodnik wykonując instrukcję ze swojej części pamięci nie może tego zmienić (#GP fault gdyby śmiał próbować). Po prostu polecenie zmieniające cokolwiek będzie miało deskryptor (MMU musi skądś to załadować) z którego przydziału pamięci jest pobrane i nie zostanie rozpatrzone pozytywnie. Dlatego kernel musi się o każde przerwanie pytać automatycznie i zrzucać wynik do wora, a wynik udostępniać na ring3 już jako przetworzone dane. Jeśli więc w procesie wstawania systemu nie kazaliście kernelowi sprawdzania co słychać u myszy i klawiatury to po uruchomieniu OS sprawa jest nie do uratowania.

Użyszkodnik może na CPU wykonać tylko instrukcje nieuprzywilejowane (arytmetyka, jumpy i podobne), ale jeśli śmie wysłać HLT lub IN to dostanie #GP. Jeśli odwoła się do pamięci kernela (bezczelnym zmień wartości w adresie) to dostanie #PF (page fault – po łapkach, że nei wolno tego ruszać). Polecenia R/W dla plików czy pamięci to są prośby na skrzynkę kernela przez syscall, czy ten byłby łaskawy rozpatrzeć uniżoną prośbę nędznego użyszkodnika w sprawie.

Jeśli w takim środowisku “na sucho” pobawicie się trochę bezpośrednią manipulacją na ten wydzielonej piaskownicy pamięci to szybko dowiecie się na debugu, dlaczego to jest tak zorganizowane. Człowiek jest po prostu nieogarem przy takiej liczbie danych jakie są obecnie przetwarzane, a dodatkowo kernel musi obsłużyć całą maszynę i czasem opóźnienia powodują, że proces wysypie się z powodu zasad uwzględnionych w kernelu, bo użyszkodnik nie wziął pod uwagę fizycznego aspektu wykonania (na przykład czasu oczekiwania GPU na komunikację z CPU wsadzając przydługą pętlę; więc w kernelu brak informacji nie spowoduje wysłania do GPU serii unidetyfied, bo user zapomniał się wypowiedzieć w terminie w kwestii uruchomionego cyklu).

Jak widać mamy już na samej pamięci przynajmniej dwa poziomy abstrakcji. A ciągle jesteśmy na etapie asemblera^^. Ring3 zwrotnie w ogóle nie otrzymuje informacji o fizycznych adresach pamięci. Przy czym spora część sterowników urządzeń ma dość prosty paging i mapowanie na wirtualne adresy więc wbicie do nich przez “odgadnięcie” adresów fizycznych jest tyle o ile możliwe. W starszych sterownikach i OSach było to filtrowane przez ring 1 i 2, gdzie istniały inne zakresy instrukcji dopuszczonych do wykonania z tego poziomu, ale udało się od tych dwóch ustawień odejść przez standaryzację i porządek. Możecie jednak spotkać ring 1 (supervisor) w urządzeniach diagnostycznych i ring2 w pewnych sterownikach kontroli dostępu (klucze sprzętowe potrzebne do uruchomienia maszyny w jakimś trybie).

 

Skoro już jesteśmy przy poziomach abstrakcji to każdy rdzeń ma swojego protosplastę sterty (heap). Jest to sprzętowy TSS pozwalający przełączać się między zadaniami (hardware task switching) z zachowaniem kontekstu. Przechowuje tam rzeczy, których nie można z pełnym zaufaniem zrzucić na wyższe poziomy abstrakcji takie jak stan fizycznych rejestrów cpu, wskaźniki stosów i ich uprawnień, flagowanie i segmenty, odniesienia/linki do poprzedniego TSS (co ja miałem na urwał myśli – gdyby zgubił wątek) oraz numery telefonu do przyjaciela (mapowanie I/O). Chodzi o to, że jak zrobicie pierońsko zagnieżdżoną funkcję (iteracja dla a od 0 do ileś, w każdym a iterujecie b od 0 do ileś, w każdym b iterujecie c i tak dalej dla wielowymiarowego “array”, a później się do tego samego “array” odwołujecie wstecznie) to nie da się jej załadować na raz do stosu i trzeba zachować kontekst o co chodziło, jaki był stan etc bez ciągłego dopytywania się w RAM jaka jest prawda na teraz. Próba odpalenia takiej funkcji w real mode oczywiście zadziała bez ograniczeń tylko będzie szła jak krew z nosa. A ponieważ TSS nie jest nieskończony to w większości manuali do kodoklepstwa macie delikatne sugestie jaki jest maksymalny poziom zagnieżdżenia. Oczywiście dziś to nikogo nie boli bo jest 8bitowy (255 wymiarów), ale historycznie było mniej, wiele kompilatorów ma ciągle hard refuse na 127 czy nawet mniej. Na starszych klockach było 15 i 31 poziomów i to też w zasadzie nie przeszkadzało do wielu zastosowań, ale przy robieniu wieloaspektowych baz danych pewnie nie raz wyszło, że musicie podzielić proces na etapy, bo wszystkich zapytań na wskroś przez dane klocek nie zrealizuje. Oczywiście był to poważny problem w analizie spektrum i jako, że jest to absolutnie konieczna rzecz w telekomunikacji, telemetrii i awacsach to dla zachowania real time między innymi w procesie SAR (Synthetic Aperture Radar) ciśnięto ten aspekt obsługi klocka bardzo poważnie. Ważny interes społeczny^^

//Wspomniałem o bramkach TG. Wcześniej.

One właśnie są związane z TSS i zajmują się przełączaniem do następnego zadania (trap gate, interrupt gate wiodące do dedykowanego – to te w prostych zagnieżdżeniach gdzie zmieniacie uprawnienia, ale nie zadanie) lub po opisie (task gate descriptor) jak jump czy call, co powoduje zapisanie do TSS co tam wyszło w obecnej sprawie i przełączenie się do innej roboty. Ponieważ można tak skomplikować (kalkulacja interferencji z wielu źródeł) proces że trzeba na raz wciskać zapisy i skoki warunkowe aby mieć się do czego odnieść, a najczęściej stoi za tym dużo matmy na FPU to rdzenie na których jest to odpalane zazwyczaj stoją dęba i nie ma im co stawiać semafora żeby reszta czekała tylko jak skończą – to będzie. Obecnie i tak się mało stosuje CPU do tego typu zadań, najwyżej do wstępnego wyprostowania wyników raw z urządzeń (czyli wartości z asteriksem lub kropką) na przetworzone, zwalenie tego na bufor i przegnanie do liczydła w GPU. Ale przez jakiś czas był to problem rozwiązywany po uważaniu i dlatego TSS jest tak rozwinięty, a potrafi zamulić rdzeń na długo. To jest ten zestaw zadań, który zazwyczaj wymaga rozumieniu zarówno co robi kompilator jak i co wyjdzie na klocek tak aby ten strumień danych popłynął w rozsądnym tempie, ponieważ przy prędkości liczonej w tysiącach metrów na sekndę prędkość bywa kluczowa.

Dlatego z braku sprzętowego rozwiązania początkowo łatką na zapas procesów do wykonania w zagnieżdżeniach była sterta (heap). Obecnie w OSach unika się overheadu zwalanego na TSS, a przełączanie softwaerowe jest elastyczne i ma większą wydajność (problem wymieniałem wcześniej – trzeba rozumieć co zrobi kompilator, i co z tego wyjdzie na maszynę oraz jak zostanie wykonane co jest obecnie w kategoriach “umiejętności przodków”). Czyli dodane poziomy abstrakcji wynikają z ograniczeń ludzi, a nie sprzętowych.

Interrupt gates zmieniające ring uprawnień w TSS znajdziecie na przykład wtedy kiedy kernel pyta “co tam u myszy, co tam u klawiatury” czyli dowołuje się I/O i przewala te raw data do uprawnień użyszkodnika (tak, żeby mógł sobie tego używać bez dodawania poziomu abstrakcji). Jeśli rozbierzecie strukturę softwearowego hepa a w locie, to na końcu każdego bloku jest zwolnienie IRET czyli wróć do swojego poprzedniego zadania zgodnie z TSS, a tam najpewniej jest już kolejna paczka ze sterty, chyba, że rdzeń właśnie miał w planie sprawdzić co tam słychać w urządzeniach (o godzinę się zapytać, o temperaturę i stany napięcia, wysłać do Chin informacje co knujesz).

Istotną różnicą pomiędzy TSS i stertą softwearową jest to, że TSS jest per rdzeń, a heap per proces. Pomijamy tu rozwiązania embeded i NUMA gdzie można sobie zorganizować to nieco inaczej w zależności od potrzeb.

Odpalamy apkę i… alokowane są dresy wirtualne z podziałem na segmenty text (instrukcje, kod po prostu, zwyczajowo przyjmijmy, że w ASM i nie kopiemy się z interpreterem), data (zmienne globalskie i statystyka), a po tym mamy dwie przestrzenie pamięci rozwijanej dynamicznie czyli stos (stack rosnąco w dół | za każdym razem jak w tym kontekście piszę stack to chodzi o thread stack, nie to co się dzieje na CPU przed dekoderem instrukcji) gdzie zwalamy zmienne lokalskie, argumenty/parametry funkcji, adresy zwrotne (gdzie oddać wynik). I stertę (heap rosnąco w górę) zajmujący się alokacją dynamiczną i tam grasuje śmieciarz (garbage collector) penetrujący referencje (co opisałem dużo wcześniej w tekście).

Rezerwacja adresów odbywa się przez brk, mmap zależnie od OS ale rozwijana jest leniwie. Dlatego dobrze jest od razu na starcie apki wyznaczyć ile ma apka zagospodarować przestrzeni. Przy odwołaniu do przestrzeni spoza zakresu (za dużo danych) pierwszą odpowiedzią CPU jest #FP i wtedy do kernela wysyłane jest demand na zmapowanie dla procesu większej pamięci oraz odesłanie adresów. Objawem tego jest to, że jeśli program dynamicznie zmienia ile mu tam niby kwater potrzeba do zasiedlenia to zaczyna mulić (z zasady przy starcie programu) i wysyła sobie z kernelem korespondencję, a kernel jednocześnie przestawia “mebelki”, żeby to jakoś schludnie, a nie wyspowo w pamięci wyglądało. Więc to nie jest tak, że apka przy starcie muli, tylko zależnie od tego co robią inne programy w RAMie może tam być deko fragmentacji i kernel zamula robiąc porządki. Dlatego warto z góry wiedzieć ile apka ma zająć RAMu i nie ruszać. Kto by się tym dzisiaj przejmował – user experience mamy w d.

Wesołe rzeczy dzieją się kiedy braknie pamięci i to akurat nie tej wirtualnej (bo kernel obecnie ma zdolność włażenia z butami na SSD i twierdzenia bezczelnie, że to RAM jest; no chyba że mu zabronicie) tylko dyskowej. Na przykład kompilacja nie mieści się na target SSD pod sata. Znaczy na końcu tego kabla więcej alokacji nie ma (bo zrobiliście ich dużo testując apkę). Przy dobrych wiatrach to kompilator się po drodze jargnie, że mu CPU wywalił “na palmę” i zwróci debug, że się nie udało. Zazwyczaj bez podania przyczyn i się domyśl. Ale kompilatory dla sapiens tego nie zwracają, po prostu kładą proces i się domyśl. Dlatego są tak mało popularne.

Jeśli komuś ciężko sobie dalej to wyobrazić to może rozpiszę, bo zdarzają się nieporozumienia co tam w pamięci trafiło na skład. Sterta przechowuje wszystkie funkcje jakie odpaliliście w każdym wątku programu – jest współdzielona rosnąc w górę. Do każdej funkcji w dół jest doklejany thread stack (już nie współdzielony), więc bardzo miło by było, gdybyście używali funkcji rekurencyjnie (stwórz stack, zapisz wynik, stack void), a nie odpalali jako kolejną instancję tej funkcji, bo każda wygeneruje sobie stack, mimo, że w stercie to ciągle jest ten sam zestaw. Zapewne zauważacie już ból d związaną z oop. Nie chodzi o to, że oop ma taką wadę w sobie, tylko o to, że homo rzekomo sapiens nie przejmując się takich zachowaniem alokatora pamięci wali instancji pod korek i dziwi się, że muli (bo dysk już RAM udaje). Zamiast rozwiązać problem “jak robić, żeby tak się nie psuło” to wprowadzili mutowanie funkcji. Czyli razem z konstruktorami dostali kolejny poziom abstrakcji, bo małpy łyse nie umieją porządku w przekładaniu kwitków utrzymać.

To zapewne jest zmiana kulturowa z gatunku opisywanej przez Dukaja w Popiśmie. Ludzie którzy uczyli się porządku nie robili tego wirtualnie, tylko te kartki, segregatory, szafy i archiwa naprawdę były problemem. Nie że jakimś wirtualnym tylko fizycznie istniały i ten bajzle trzeba było jakoś ogarniać bez alokacji w nieskończonych magazynach papierków. Więc kodoklepca, który gdzieś tam pierwotnie wyrasta z księgowego lat siedemdziesiątych tę dyscyplinę i rozeznanie co się dzieje z danymi miał. Bo organoleptycznie miał kontakt ze zjawiskiem IRL. Że to zajmuje miejsce, że demony się grzeją i entropia hula po pamięci. Oderwanie związku IRL od braku wyobraźni u kodoklepców kolejnych pokoleń, które nie wiedzą dlaczego ikonki mają taki kształt (bo nigdy nie widzieli archiwum z papierami) spowodowało, że nie mają ani potrzeby, ani nawyku utrzymania porządku w generowaniu instancji. Przecież tym się zajmie inny proces (niższy poziom abstrakcji, tu kernel se to ogarnie). Owszem ogarnie, ale nie za darmo – demon się grzeje, entropia hula, nic nie działa i nikt nie wie dlaczego. A na CPU trafia sbrk o rozszerzanie adresów hurtem. Po czym trzeba było dodać to tego śmieciarza (garbage collector) bo przecież sprzątać ma się samo. No samo się posprząta, ale cykli ile zeżre^^

Ostatnio nawet na dość złożonym kodzie (jakieś 25k funkcji) unieruchomiłem GC i okazało się… że leaka nie ma. No ale sam se tego potworka zrobiłem. Jakby klepaczy było siedmiu to obawiam się, że byśmy po roku nie doszli kto i jak do leaka doprowadził. Więc byłby GC w akcji co ileś cykli.

Sposób w jaki to trafia ze sterty i stosy (softwearowego) na kolejne warstwy L3 do L1 odbywa się przez serię pointerów (wskaźniki) i ich kopie. Na stercie pingwina jest int* p = malloc(sizeof(int)); p ląduje na stacku;int x = *p; idzie na rejestr i stack (już ten prawdziwy, w CPU) używa MOV żeby sobie ze sterty wrzucić czystą wartość do wykonania w L1. Z jednej strony ładowany jest kod (fetch, tu akurat prefetch) przy użyciu LOAD (I-cache), z drugiej argumenty (no te właśnie ze sterty zwalonej na stack soft, zwalonej do L1) przy użyciu MOV (D-cache). Otwieramy ramkę stacka cpu PUSH i tam wieziesz paczkę LOAD instrukcja MOV argumenty rekurencyjnie. CPU zajmuje się więc w dużej mierze organizowaniem sobie rozkazów do wykonania. Bardzo duży pary w gwizdek przy tym idzie jeśli kompilacja nie była wykonana na rozsądnym kodzie.

 

Tej bzdetnej opowieści, gdzie jedni rabini mówią tak, a inni inaczej już koniec. Pewnie doklepię tekst o stosunku między wysokim poziomem abstrakcji kodu, kompilatorze, dekoderze i opkodzie, czyli dlaczego mamy coraz szybsze kompy i coraz wolniej działające programy. Oraz o tym gdzie powinno być wciśnięte NPU i jakiej nowej architektury możemy się spodziewać po przepychankach Czajników z Jankesami, bo że ktoś wprowadzi nową to pewne i konieczne (x86 jest mocno zaprzeszły, arm jest mocno niepraktyczny). A tym razem padło na to, że zrobią to właśnie Chińczycy. Więc będzie glimpse of the future gdzie trzeba wstawić NPU tak aby wszystko zmienić i z naszego punktu widzenia zostało po staremu.