Mainstream

tryby rzeczywistości 8/9 #Co tam w rdzeniu są za kable?

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

#Co tam w rdzeniu są za kable?

W stanradzie (8086) zaczęło się od mniej niż 30kilo tranzystorów. To czemu jest ich teraz dwa miliardy i czym się zajmują (oczywiście szpiegują dla reptilian). W zasadzie to niedużo… W tym # omówię ALU, FPU/SIMD, MUK, AGU, BPU, ASH, CU, S/LU. To są oczywiście nazwy logicznych abstraktów układów, które historycznie istniały w tej czy innej postaci, obecnie są to dynamicznie łączone kawałki układów podłączane wg wzorca uops do ciągu wydarzeń. Ale nie będę wchodził w te technikalia tym razem. Kilka razy w tekście poniżej się o to potknę, że to trochę inaczej jest obecnie rozwiązane w praktyce (gdyby to było tak proste, to klocek miałby rozmiar pokoju, ponieważ układy zaczną się rozmnażać wzajemnie jak króliki ze względu na potrzebę odwołania się do logiki, addera, multiplera czy floata). Jak widzicie z listy to jest opis dla CPU, w GPU mamy jeszcze CUDA (na kiju), który nawet nie jest wskazany, gdzie konkretnie występuje jako układ (tajemnica producenta).

AGU to centrala poczty, adress generation unit. Jeśli włączycie stronicowanie (CR3) to zajmuje się liczeniem gdzie fizycznie jest segment + offset, oraz w drugą stronę „o jaki adres abstraktowi chodziło?”. Zaczęło się od dwóch oddzielnych układów (jeden odczytu, drugi zapisu), ale szybko sprasowano to w jeden z opcji (dlatego ten decoder unit – pan chce telegram nadać czy odebrać?). Ma swój własny adder (jak wpisujecie w buffer peek/poke segment+offset to on po to ma int addera, aby ogarniać ten offset) i nadpis (maskowanie adresów, tych 4kilo wspomnianych dużo wcześniej). Operowana długość wprost wynika z odpalenia trybu FPU, w real16 ma pozamykane ścieżki. Zwraca ścieżki do L1Dcache i TLB (Translation Lookaside Buffer). Jedynym powodem wpakowania tych 50kilo tranzystorów na jednostkę (a na nowszy rdzeń są tak ze dwie przynajmniej) jest tylko i wyłącznie abstrakt stronicowania pamięci potrzebny wyłącznie homosapaczom, któryz nie chcą przewidująco napisać programu tak, żeby fizyczny przydział adresów im starczył. Ponieważ 50kilo przełączników generuje ciepło to zgadnijcie czy w teleskopie lecącym w kosmos, który ma być zimny wsadzane są takie zbędne grzejniki i czy kod dało się jednak napisać schludnie. Ta proteza dla zawartości krzesła dokłada cykl lub dwa do każdego polecenia nawet jak wpiszecie głupie mov rax,rbx;

MUK – Pokémon trujący, przypomina wielką, fioletową masę toksycznego szlamu. Powstał z zanieczyszczeń i odpadów przemysłowych.

BPU – przewidywacz jaką następną gałąź (jeśli jest branching w kodzie) przewidziała małpa do złapania. Branch Predictor Unit. Obsługuje JNZ i CALL ale nie w real16 (wtedy sobie po prostu trzeba poczekać, aż kod “się przewinie” do funkcji (4cykle skoku). Waży to 100kilo tranzystorów, i jedzie po bitach historii Taken/Not w Branch Target Buffer w SRAM. Potrzebny jest też (tylko i wyłącznie) z powodu drzewek kodu, kiedy nie cały jest wykonywany. Czyli kiedy napiszecie program, który nie operuje w cyklu po całej zajmowanej pamięci. Czyli w zasadzie zawsze? Bo raczej nikt nie pisze take a leave a tylko przeskakuje if a to olej? No i z tym właśnie predyktor sobie radzi, żeby szybciej przewinąć linijki “asm” do tego co małpa miała na myśli. To jest kluczowy element superskalarności w nowych klocach. Oczywiście to jeden z predyktorów, ten ostatni, zarządzający wykonaniem (pozostałe zajmują się grabieniem RAM na cache i żyją w kontrolerach pamięci przewidując zmiany w długości wzywanych danych, tak żeby dwa razy nie wołać pasącego się DS). Kolejny przypadek, kiedy to zachowanie kodoklepstwa wpływa na konstrukcję maszyn, tak żeby się nie kopać z małpami. Na szczęście jeden na rdzeń starcza.

Niby BPU to duperel, ale w robiąc na GPU (przynajmniej na starszych glsl es, które w większości bezpiecznie ruszą na androidzie) zauważycie, że nie ma tam polecenia switch. Trzeba po staremu serię if, elsif, else.

ASH – Ash Ketchum z Pallet Town, jego pierwszym pokemonem był Pikachu.

CU (“porządkowy” control unit) – na każdym rdzeniu ktoś musi pilnować porządku. Obsługiwać wyjątki (general protection czy zero diveder, co często oznacza po prostu wynik zbyt duży dla rejestru docelowego). Te sto tysi tranzystorów zajmuje się podłączeniem prądu do pozostałych układów, oraz flagowaniem danych, wyborem sekwencji mikrokodu (na przykład jest polecenie, że będziemy dzielić – to alu trzeba włączać, i oba rejestry flagowane float to będziemy włączać w alu obsługę stanrdu dla float, i w tym drugim rejestrze jest zero… a to nic nie włączamy, spadaj, dzielenia przez zero w tej firmie nie robią). W trybie real nie jest włączony w całości, steruje tylko sygnałami z dekodera, które są krótkie, rzadkie i proste. Ale po włączeniu reszty klocka ma nieco więcej roboty (przydział mocy, uruchamianie urządzeń). W trybie real jego głównym zajęciem jest rozwiązywanie zagadki “jakie jest następne polecenie przy warunku jump?” czyli wybór sekwencji, co w takim plain kodzie asm oznacza “ile linijek dropnąć, bo polecenie spełnia warunek”. Ponieważ w trybie rzeczywistym nie ma potrzeby sterowania prądem (większość klocka nie jest podpięta, instrukcje schodzą w długich pętlach NOPów) to klocek jest przez chwilę zimny (aż do odpalenia sekwencji bios/uefi i włączenia reszty pozwalającej wyzyskać puste cykle czekania fetchując dane na zapas).

FPU – dochodzimy do czegoś fizycznego. Ponieważ jestem starszy od obecnego standardu IEEE754 to pamiętam też inne. Novel czy IBM (firma od oznaczania w dowodach osobistych czy masz pochodzenie semickie) miały własne, inne, ówcześnie obsługiwane głównie softwearowo kombinując z paczką bajtów gdzie rozrzucano flagi, cechy i mantysy. Fizycznie był to w standardzie 8086 zewnętrzny koprocesor 8087. On w rdzeniu ciągle jest (kompatybilność wsteczna), ale raczej go nikt nie woła. To trochę bardziej skomplikowane z powodu mikrokodu [obiecałem o tym nie pisać] – taka zewnętrzna jednostka po kawałku potrzebna była wewnętrznie w wielu procesach. Początkowo mikrokodu nie było, więc fizyczne ustawienie kabli prowadziło do właściwego sygnału zwrotnego. Każdy układ logiczny był po prostu przejściem przez labirynt, ale pewne sekcje labiryntu zaczęły się powtarzać. I to wielokrotnie. Klocki zaczęły się paść, powstawały kooprocki (jak ten) i sam proces odwoławczy do urządzenia rachunkowego (lub logicznego – harwaerowych “tabelek prawdy”) mroził klocek na kilka cykli (zgodnie z dokumentacją add int powinno zająć cztery cykle, a na najnowszych klockach zajmuje siedem co nie jest problemem, ponieważ tych obliczeń na raz wykonywanych jest hopsztylion, a zwrot do pamięci lub innych urządzeń i tak trwa dłużej niż te kilka cykli bo rdzeń jest tak mocno taktowany). Do tego większość poleceń odwołuje się do identycznych układów “policz mi!”. Więc w takim klocku trzeba by rozsiać int addery w zasadzie wszędzie, żeby wybrać linię kodu, żeby wybrać adres pamięci, żeby zdecydować czy rejestr jest wolny etc. Ponieważ przed wykonaniem tych operacji klocek i tak by nic innego nie robił (bo mu jest ta zwrotka potrzebna do zrealizowania całej operacji) to trzeba było to “jakoś” pokolejkować i nie mnożyć identycznych partii labiryntu nad potrzebę, ponieważ na raz i tak będzie działać może ze cztery (dlatego mamy tyle ALU ile mamy i każda ma własny FPU w wersji nieco przyciętej mikrokodem, bo one się wszystkie odwołują do kilku identycznych układów FPU z ograniczoną funkcjonalnością i jednego z pełną + rozszerzenie o generatory wartości .math przy użyciu ścieżek w labiryncie sterowanych prze uopsy – mikrokod). Będę się w tekście odwoływał do tego w taki sposób, jakby każdy układ miał własny podzespół, ale to nie jest do końca prawda – trochę jest, ale w pewnych warunkach nie jest.

Trochę historii o tym jak zabierano się do tej żaby, choć to było całe zoo formatów (dosłownie). No więc na początku (nim pierwszego września roku pamiętnego wróg napadł Niedorzecze z kraju ościennego) Konrad Zuse:

https://tron.fandom.com/wiki/Castor

wprowadził 24 bitowy single precision na podstawie 2 format obsługujący znakiem (1), wykładnikiem (7), mantysą (16), ukryty (1) w mantysie; obsługuje liczby dodatnie, ujemne i nieskończoność. W tym formacie dawało się rozpoznać undefined [mul(0,infinity)], ale nie było obsługi NaN. Nadmieniam o tym, ponieważ @gruby czasem wspomina o Z1/Z3 i to właśnie na tych maszynach była obsługa tego formatu.

IBM długo później wprowadził własny format (po serii błędów i wypaczeń) na bazie szesnastkowej w precyzji single i double (32 i 64) bazując na rozwiązaniu Zuse (paperclip op) robiąc tak samo 1+7+24 i 1+7+56 czyli wykładnik pozostawał ciągle siedmiobitowy, znak został tam gdzie był. Jest to format o tyle ciekawy, że ta długa mantysa reprezentuje cyfry szesnastkowe i ma zmienną precyzję z tego faktu wynikającą. Ma też te same wady co u Niemca jak obsługa dziwnych poleceń podziel nieliczbą (NaN) przez infinity oraz innych zachowań paranormalnych, które musiały być obsługiwane w kodzie wcześniej aby je wykluczyć.

DEC VAX był takim prototypem IEEE, praszczurem jego w pewnych rozwiązaniach. Jeszcze bez pełnej obsługi NaN, ale coś tam już zaczynało świtać, choć zaczęto kombinować z różnymi proporcjami cechy do mantysy w bitach, czyli z tym zmiennym przecinkiem na poważnie, choć na początku w dwóch tylko trybach D i G na proporcjach 8+55 i 11+52. Miał sporo wspólnego ze wcześniejszym UNIVAC, kiedy jeszcze nie zdecydowano się ile tych bitów trzeba i było niestandardowo (po pamięci ciągłej, jeszcze się wtedy na bajt ośmiobitowy nie zdecydowano). Podobnie jego praszczur znowu CDC i te moje żarciki o zerach ujemnych i dodatnich.

M$ też wypuścił swój MBF, ale tam wszystko było z d, mantysa przed cechą i podobne kwiatki jak znak między nimi.

Były też formaty nieprzekładalne (port ciężko zrobić) dla popularnych w przeszłości superkomputerów. Taki Cray na przykład miał własny format single precision 64, bez ukrytych w mantysie bitów. To były takie wyjątki do określonego zastosowania.

Zdecydowano się na bazę dwójkową (mniejsza precyzja, prostsze “kable”) i początkowy pomysł Zuse czyli reprezentacje specjalne (infinity). Istotne okazało się, żeby to było przekładalne (portowalne) i zaczęto poprawiać VAX G czyli 1+11+52 ododając NaN oraz zaokrąglanie precyzyjne (na int) bo to był wielki ból d. Z zaokrąglaniem precyzyjnym chodzi o to, że dla porównania liczb przy ograniczonej precyzji cechy i mantysy (bo bity) trzeba dopełniać algebrę a-b=0 jeśli a=b, a jeśli puścić to tak na żywioł przez ALU to się nie uda. Dla świętego spokoju przestano kobminować prz innych rozwiązaniach, bo już byl etap obsługi tego bibliotekami numerycznymi i możliwość interpretacji wyników “po swojemu” więc przekładalność pomiędzy masyznami miała większą wagę niż zatosowania specyficzne. Dziś jest to domyślny tryb obsługi float, a wielu młodych jest przekonanych, że innego nie ma. Na bazie tego pozostał w klocku kooklocek 8087 z własnym stosem (length 8 na początek^^) i gdybyście potrzebowali się z jakiś przyczyn (wywoływanie duchów) doneigo dogrzebać na poziomie rzeczywistym to zaczynamy od polecenia ESC (wyjdź, czyli nie do Ciebie mówię teraz CPU, teraz będę przemawiał do tego drugiego, karm go następnymi informacjami) i FLD (załaduj floła) podajemy precyzję (m32,m64,m80 – jeśli kojarzycie naście/dziesiąt stron wcześniej jak wspominał o 80 bitowym problemie), FST na stos, FSTP ze stosu (pop) i możemy podać precyzję. FIST/FISTP to samo tylko, że bawimy się w liczbę całkowitą (int), FBLD/FBSTP do zabawy w stoś length 10 dla m80, FXCH do przetasowań na tym stosie (albo z innym rejestrem). FADD (i mutacje) każe dodawać, FSUB (i mutacje) odejmować, ale możecie odwrócić kolejność (FSUBR) co się przydaje w pewnych optymalizacjach ładowania z listy albo bezczelnej obfuskacji w kompilatorze. FMUL i mutacje mnoży i tak dalej… ma nawet własny układ pierwiastkowania (ale tylko po kwadracie) i zaokrąglania. FCOMy i FUCOMy do porównywania (ten drugi radzi się z porównaniem do NaN), w kompilatorach raczej używa się tego drugiego chyba, że pracujecie na reprezentacjach bitowych i majstrujecie coś dziwnego. Tutaj też liczona jest cała trygonometria, logarytmy o podstawie naturalnej (sprzętowo!) i liczby na lewo od potęgi dwójki (wiadomo że 2^8 to max 255). Ma własną pamięć (dokładnie taką jak w starym kalkulatorze) i przywracanie z pamięci.

Najistotniejsze jest zaś co może zwrócić. Wcześniej wspominany #DE (overflow lub dzielenie zerem), #IA (delikt pierwiastka z liczby ujemnej, NaN), #O przegiąłeś pałę, overflow, #U – bitów brakło, dowieź dane; #P precyzja poleciała, #Z dzielisz zerem. A ponieważ to wszystko jest obsługiwane, to fakt iż w kodzie się machniecie (na przykład przy wyznaczniku macierzy nie wpiszecie obsługi zera) i wywali error, zamiast zwrócić do obsłużenia flagę, co poszło nie tak świadczy tylko o tym, że ktoś przy kompilowaniu się lenił i poszedł na łatwiznę uznając, że każdy błąd to po prostu błąd i nie ma co roztrząsać. Sporadycznie wywali Wam też błąd kiedy mocno operujecie na pamięci i po prostu potok nie dowiezie. Wtedy po Waszej stronie pozostaje catch/try, bo komuś się nie chciało porządnie kompilatora napisać (tak, żeby zwracał Wam tę flagę i co wtedy, bo na przykład czasem można repeat – nie dowiozło w potoku bita i #U wróciło; i predyktor sobie to obsłuży nie zrywając potoku).

Oczywiście jeśli macie gołe 8086 i wbijecie ESC do obsługi kooproca to wywali #UD, że nie ma takiej instrukcji zdefiniowanej (bo kabla niet). FPU działa w pełnym syncu zegarowym z core klocka i pozwala to na równoległe wykonywanie operacji na obu (wait). To jest po prostu dodatkowy procesor, który może sobie robić “co innego” bo tylko do tego służy i do tego ma architekturę. Dostaje on szereg zadań i danych z rejestru i ma wrócić z wnioskiem jak już sprawę przemyśli. Jest tak cholernie potrzebny, że… prawie się go nie stosuje. Ponieważ wszystkie układy, którego by go potrzebowały mają jego uproszczoną wersję (bez wszystkich funkcjonalności bo ma działać tylko na jednym formacie danych i nie musi obsługiwać wyjątków) i dlatego w kompilatorach włos nie jest dzielony na czworo (no chyba, że istniała taka potrzeba). Ten kooprocek jest zaprzęgany głównie w przypadku kiedy nie wiadomo po kiego coś jest liczone (po prostu jest liczone) albo użyliście jakiejś funkcji z zakresu math czy akurat wymusiliście high precision dla zmiennej float. Dodatkowo ten kooprocek nie ma pełnej implementacji IEEE754 i obsługa pewnych udziwnień oraz zaokrągleń bywa odmienna od spodziewanej w dokumentacji. Wolno na nim robić rzeczy niestandardowe.

Naturalnie w dzisiejszym klocku (o ile nie bawicie się elektroniką) siedzi któryś z jego potomków (80287 czy 80387) ale w obu wypadkach mówimy o urządzeniu logicznie starszym od części czytelników.

Na zakończenie o floating point dodam, że nie jest to jedyne rozwiązanie, ma ono swoje wady (przestrzeń reprezentowanych liczb, dynamika tej przestrzeni, precyzja w reprezentacji związana ze statyczną podstawą biasu, liczby wykluczone z reprezentacji, operacje proste i wqurwiające). Wyszło ono z fixed point (radix) w reprezentacji bitowej (Q), później był ruchomy radix, później zamiast reprezentacji bitowej wprost funkcja znak/cecha/mantysa. Na etapie jego wdrożenia był to format wystarczająco prosty, wystarczająco wsparty sprzętowo, wystarczająco powszechnie rozumiany. Ale nie jedyny! Rozszerzeniem IEE754 jest unum (ruchomy rozmiar cechy i mantysy) oraz posit (reżim, cecha, mantysa; nadaje się zarówno do quantized int jak i float). Unum i posit to pomysły człowieka z Caltechu, który stworzył to co nazywamy datacenter, stworzył pierwsze usługowe zwierzę z tego gatunku. Mamy też LNS (do mnożenia/dzielenia bo na logarytmach, ale do dodawania i odejmowania się nie nadaje), dawniej używane we wzmacniaczach (TMS320 był przed wprowadzeniem IEE754), dziś to raczej macie w telefonie do rozpoznawania mowy i filtrowania sygnału, a w ejaju do konwolucji sieci (tam we wuchtę mnożenia jest na dużych liczbach), nie zawsze jest to rozwiązane sprzętowo (choć to akurat jest proste) tylko emulowane w kodzie, ale przydaje się. BFP (stosowane w gpu gdzie cecha wspólna, mantysy osobno, czyli zabawa w tensory raczej wyższych rzędów; więc słusznie podejrzewacie że w ejaju to kwitnie) i dyskretyzowanie po całkowitych [quantized int; real_value ≈ (int_value – zero_point) * scale ] też w ejajach (dlatego rejestry int8 są w tych super duper kartach o które tam mainstram nawija na Jankesów, że nie chcą sprzedawać ile się komu zamarzy). Ułamki łańcuchowe raczej służą reprezentacji liczb, bo liczy się na tym paskudnie.

Po prostu wszystkich implementacji nie robiono w sprzęcie, kiedy format się uleży i zacznie boleć to Intel czy AMD doda obsługę w mikrokodzie (CISC) dla ALU, a RISC jak zaboli to po prostu doda klocek, który to robi, i kolejna generacja procków (jeśli się rozwinięcie przyjmie) będzie już miała dodatkowy moduł dedykowany tej operacji z własnym mikrokodem do przyspieszenia stodunku. Chciałem tylko zaznaczyć, że rozwiązanie istnieje, ale musi boleć na tyle, aby producent klocków chciał to implementować sprzętowo. Raczej nie odbędzie się to na CPU bo obliczenia obecnie przerzucane są do innych układów.

SIMD jest rozszerzeniem po szerokości (bo można się rozszerzać po długośc, albo w głąb^^) omawianego FPU do wykonywania tego samego w ograniczonym zakresie, za to hurtem. Działa na bardzo szerokim rejestrze, jest duży, bierze dużo prądu i jeśli zbierzecie dość floatów aby to była macierz i chcecie ją przepychać nie w kolejności tylko wszystkie liczby na raz, to się właśnie tu odbywa sposobem takim jak powyższe FPU bez obsługi wydarzeń, które się w tego typu kalkulacjach nie dzieją).

No i wbijamy na rafy… ALU;

To jest najczęściej używane liczydło. Jest ono “wewnątrz” klocka (nie tak jak FPU) x86, a w kolejnych iteracjach rozwoju pasło się coraz bardziej. Początkowo to było 10k tranzystorów i jedna jednostka, obecnie przekroczone jest już 100k i są ze cztery na rdzeń. To tutaj wykonywane jest dodawania, odejmowanie, mnożenie, oraz rozwiązywana logika (na przykład mnożenie przez dwa to nie mnożenie tylko SHL – shit^^ left i wychodzi na to samo). W kontekście istnienia FPU8087 możecie się dziwić jak do tego doszło. No więc to początkowe alu to tak sobie na integerach po 8bit chodziło i oczywiście trzeba było je rozbudować. Ale ze względu na potrzeby i rozmiar klocka w czasach dawnych (oraz brak standaryzacji) mniej roboty było, żeby motorola czy intel dokleiły sobie oddzielny kooprocek do liczenia niż rozstrzyganie jak ma działać rdzeń. Dlatego z dawnych języków możecie pamiętać polecenia .math, które kierowały poza klocek. W samym rdzeniu na alu matma była zrobiona po łebkach – aby była, żeby się dało policzyć adres, index i podobne duperele sterujące czyli głównie liczby całkowite. Przy wstawaniu klocka całe ALU nie jest włączone. A działa to tak…

Dla operandów są dwa wejścia główne i jedno na opcode (instrukcję). Na tablicach w szkole mieliście to rozpisywane jako operand A i B – w tej kwestii nihil novi. Dodatkowo wchodzi rejestr flagi (opcode), czyli pewnej wariacji instrukcji, zazwyczaj dotyczy danych na wyjściu. Polecenie ADD int otwiera bramki sumatorów z opcode dla carry (czyli przenoszenie overflow do kolejnego sumatora), ta część układu jest na tyle prosta, że można ro zrobić w domu bez specjalnych kwalifikacji, jakoś tam będzie działało. Mamy tam serię bramek logicznych bit po bicie A XOR B AND A XOR B AND… aż rejestru braknie. Czy wbijecie na rejestr liczbę zajmującą cały czy też używacie tylko kilku pierwszych bitów bez znaczenia – zadziałają wszystkie bo to po prostu jest stan napięcia. Ponieważ działa to w korytarzu sumatorów (tryb rzeczywisty) to zajmuje cykl na dwa bity, czyli że tak powiem… ślamazarne jest. Ale dla FPU32/64 bitowego jest adder szeregowy, który wypluwa wynik w jednym cyklu.

Odejmowanie to inwersja (B XOR 1 z przeniesieniem carry), a później wystarczy dodać A. Więc w trybie rzeczywistym zeżre cykl na bit, ale po uruchomieniu trybów szeregowych FPU32/64 będzie w jednym cyklu. Jedyny powód nieodpalania trybu szeregowego w trybie rzeczywistym to kompatybilność wsteczna z paragrafu “tak zawsze działało, i tak używamy tego ułamek sekundy przy stacie zanim UEFI zrobi porządek”. W zasadzie można to przeprojektować i nic się nie stanie, ale chyba tylko w arm się komuś chciało chcieć.

Logika bitowa jest na prostych, zrozumiałych wprost bramkach tak samo jak multipleksery (MUX) przesuwające zawartość rejestru. Gdyby ktoś to czytał nie wiedząc o co chodzi (i aż tu dobrnął) to tabele prawdy wygooglać i będzie zrozumiałe.

IMUL to pętla dokładnie taka sama, jakiej uczyliście się w szkole licząc słupki – dodaj, przesuń, dodaj. Po prostu im dłuższe liczby wejścia tym więcej pętli adderów i shifterów i tutaj jest kółeczko do początku, bo multipler potrzebuje addera do zamknięcia pętli. Jeśli w trybie rzeczywistym wrzucicie mul(15,15) to będzie to 70 cykli nad tym móżdżył na korytarzach pętli, a szeregowo to zajmuje trzy cykle (FPU32/64). Dzielenie też działa dokładnie tak jak słupkami w szkole, czyli loop odejmowania i przesunięć. O ile było to w pierun wolne dla trybu rzeczywistego (wolniejsze niż dziś dostęp do RAM, a to wewnątrz klocka jest osiągnięcie), o tyle wiele się w tej sprawie zrobić nie da i nawet z opcodem na nowych klockach nie schodzi się niżej 20 cykli. Jest to najbardziej energożerna funkcja w ALU dla int, a dla float trzeba ją jeszcze pomnożyć.

Na chwilę skupmy się na tej różnicy międyz korytarzem i szeregiem sumatora (oraz innych wariantów w domyśle). Korytarz oznacza fizyczne przełączanie prądu bramka po bramce, czyli najmłodszy bit A XOR najmłodszy bit B ma rozwiązania 0xor0 =0, 0xor1 i 1xor0 = 1 i 1xor1 = 1 + carry 1; w pozostałych wypadkach carry wynosi zero. Kolejny najmłodszy ma to samo xor carry i tak aż do końca, gdzie jeśli ostatni zwróci napięcie na carry to zapala flagę overflow (i ona jest obsługiwana albo nie, bo tryb rzeczywisty niekoniecznie musi mieć tam kabel zapięty i słuchać). Wynik jest jaki jest i to jest zupełnie logiczne. W trybie szeregowym zaś używany jest rejestr flipflopowy spięty tak ze setką tranzystorów na 64 bity i jako wynik schodzą dwie liczby – wynik z każdego Abit XOR Bbit oraz Cbit (carry). Logicznie byśmy teraz wrócili do pętli xor, ale… dzięki spięciu rejestrów w poprzek tranzystorami wiemy skąd dokąd pędzie propagacja carry (bo jesteśmy na flipflopach i na nich jest stan napięcia, a nie przepływ, i dlatego jest dawane im więcej prądu na upływ dla tych tranzystorów) więc wiemy które grupy obejmuje przeniesienie (i to od razu wiemy wszystkie) oraz gdzie wybije carry. Więc zakresy jedynek przez które leci przeniesienie są odwracane (czyli 1+1 daje zero), a na następnym gdzie oczekujemy zera mamy flip na jeden. Co najłatwiej wyobrazić sobie jako negatywną maskę carry i przesuniętą maskę carry. Ponieważ z tego rozumowania wychodzi Wam od razu, że jest to system predykcyjny to oczywistą sprawą jest, że jeśli dość skomplikujemy dane (w złośliwą kratkę) to mogą się pojawić artefakty. Na liczbach całkowitych one się nie pojawią, ale jak zaczniecie kombinować z robieniem tego na floatach bez FBU to istnieje możliwość, że przy mocnym taktowaniu i piłowaniu sumatora na 80bitach float co jakiś czas termika wybije artefakty nie do końca zresetowanej maski. Przy czym w potoku FPU trzeba by do tego mieć bardzo powtarzalny program (do rycia coinów), który nie oszczędzi ani pół cyklu na NOPy, ale w GPU takie cuda są na porządku dziennym (ale są obsługiwane, znaczy nie zauważycie różnicy, że na monitorze na jedną klatkę, stan któregoś z kolorów na pikslu gibnął się o 1/255, a w operacjach wektorowych zrzucilibyście to na brak precyzji czy ortonormalizacji macierzy po obrocie).

No i przechodzimy na float…

FPU i SIMD są podpięte (implementowane, po prostu okrojona i poszerzona w inną stronę wersja FPU jest tu dziś wsadzona) właśnie w tym miejscu, one wcześniej obsługiwały zmienne przecinki i kaskady (wektory) przez zewnętrzny kooprocek, a obecnie mają własny (w architekturze nic się nie zmieniło, po prostu nie trzeba już na krzyż łączyć tych układów, problem jest rozwiązany w logice wcześniej, za to na krzyż połączone są inne potrzebujące liczenia).

Użycie FPU oznacza coś w stylu “policz to naprawdę”, a użycie ALU “policz to normalnie”. Użycie SIMD oznacza “policz dużo tego samego” i może być normalnie lub dokładnie (nie każdy układ pozwala na dokładnie, w potoku liczącym wcześniej kolor na ekran tej opcji nie było, przy czym tu bym musiał się rozpisać o tym jak zorganizowane jest GPU, w każdym razie jeśli SIMD jest naprawdę duży jak w GPU to jest on duży w celu i monitor jest jaki jest, kolory wyświetla z zakresu i z dokładnością, więc tam wpinanie odpowiednika FPU pod odpowiednik olbrzymiego SIMD mijałoby się z celem). Ponieważ cyrki ze zmiennym przecinkiem omówiłem przy FPU to patrz wyżej co tam się dzieje, a teraz dodam tylko jak to się dzieje, też niby było w szkole po słupkach, ale pewnie wtedy nikt nie uważał. Czyli FADD i ADDPS na początek…

Dla uproszczenia 32bit mamy znak(1), cecha (8), mantysa (23). Dla 64 1+11+52. W mantysie jest ukryty bit wiodący wynoszący zawsze 1 co zwiększa efektywną precyzję. Ponieważ to jest float odpalane jest FPU choć jego fizyczne rozdzielenie od ALU sprowadza się dziś oflagowania czy liczymy int czy bawimy się we float. Zaznaczmy, że to nie jest FPU w rozumieniu klocka 8087 tylko pewna jego wariacja. I zapinamy tak z milion tranzystorów do roboty. Nasze float mają prównanie cechy aby wyrównać mantysy i po wyrównaniu mantys (mantysa z mniejszą cechą jest przesuwana w prawo) cechy mogą być add/sub (8bitowy sumator dla f32), i mantysy tak samo (na sumatorze f24), a wynik normalizowany (przesuwanie mantys i zwiększanie cechy). Leci jeszcze dość prosta logika sprawdzająca wyjątki (infinitkę, NaN, bias zero, mantysę zero – przypadki w okolicach epsilonu wymagające normalizacji bo liczba nie jest flagowana jako zero, a poszła jako divider i wiadomo, że wyjdzie dużo, no ale nie infinitka; bez tego w trybie bez normalizacji kalkulator wypluje zero divider i zamknie sprawę, ale FPU pominie krok dodawania bitu ukrytego i poprzesuwa cechy i mantysy żeby coś wyszło; czyli żeby nie wywalić błędu dzielenia zerem i #U ~1.18e-38). Takie zabawy wokół wyjątków wciągają ekstra cykl i zapotrzebowanie na prąd w obu cyklach. Niedużo, ale odbywają się przy dzieleniu więc i tak w warunkach ekstremalnych.