Logo

 


artykuły

projekty

32 bity: CISC vs. RISC

Począwszy od pierwszej 4-bitowej konstrukcji, rozwój mikroprocesorów przebiegał zasadniczo dwiema niezależnymi ścieżkami. Pierwsza koncentruje się wokół hardware’u i zaimplementowaniu w nim jak największej liczby funkcji. Druga zorientowana jest na software, ponieważ oprogramowanie, w odróżnieniu od sprzętu, można zaktualizować. Dlatego sprzęt powinien realizować tylko to, co musi, oraz co – ze względu na wydajność – powinien.

W poprzednich odcinkach…

Wprowadzony w pierwszym cyklu współczynnik nazwany „efektywnością architektury” w przypadku konstrukcji 32-bitowych z przetwarzaniem potokowym (ang. pipelining) nie jest doskonałym wskaźnikiem pokazującym wydajność rdzenia. Szeroko dostępne, standaryzowane benchmarki sprawdzą się w tym przypadku lepiej, nawet standardowy, uśredniony współczynnik CPI (ang. cycles per instruction – liczba cykli na instrukcję) czy IPS (ang. instructions per second – liczba instrukcji na sekundę) pozwoli na lepsze porównanie wydajności procesorów. Zdaniem autora jednak współczynnik efektywności zdefiniowany następująco:

\[\text{efektywność architektury} = \frac{\text{ilość 64-bitowych dodawań na sekundę}}{\text{ilość tranzystorów w tysiącach}}\]

pozwala na dokonanie oceny samej architektury w pewien specyficzny sposób. Pozwala na oszacowanie efektywności wykorzystania elementów składowych mikroprocesora do wykonywania prostego przetwarzania danych. Wszystkie mechanizmy i bloki funkcjonalne dostępne w złożonych mikroprocesorach niekoniecznie dają przewagę na tym polu, a niejednokrotnie bywają sporym balastem. Przykładem jest mechanizm stronicowania pamięci i przechowywania jej w pliku wymiany. Pochodzący z wielodostępnych systemów serwerowych sposób wirtualnego zwiększenia dostępnej pamięci nie ma praktycznego zastosowania w dzisiejszych, jednostanowiskowych stacjach roboczych, szczególnie w dobie stosunkowo tanich modułów pamięci DRAM.

CISC: Intel 80386

Mikroprocesor Intel 80386 (iAPX 386) jest modelowym przykładem konstrukcji typu CISC (ang. complex instruction set computer – komputer o złożonej liście rozkazów). Kategoria ta została zdefiniowana dużo później niż konstrukcje, które ostatecznie do niej zakwalifikowano. W pewnym momencie rozwoju technologii trend prący do wzrostu komplikacji rdzenia uległ odwróceniu, a w kontraście do architektur uproszczonych RISC zdefiniowano właśnie termin CISC. Najczęściej wymieniane cechy, klasyfikujące dany mikroprocesor do tej kategorii, są następujące:

Wiele z tych cech ma swoje źródło w architekturach komputerów mainframe z lat 60-tych XX wieku, na których bazowały wczesne architektury 16-bitowe. Były to głównie linie DEC VAX oraz PDP.

Chęć zachowania zgodności z 8-bitowymi poprzednikami oraz trend do oszczędzania pamięci programu wymuszał stosowanie rozkazów o zmiennej długości, operujących na różnych typach danych. Dawało to w efekcie krótki, oszczędny kod o dużej gęstości. W początkach technologii komputerowych było to bardzo ważne kryterium. Pamięć była droga, a dostęp do niej wolny, więc zmniejszenie ilości odwołań do pamięci dawało istotne zwiększenie prędkości wykonywania programu. Bardziej zwarty kod, z definicji, wymagał mniej pamięci, a tej wówczas zawsze brakowało. Zwiększenie listy rozkazów oraz zróżnicowanie rozmiarów operandów wymagało rozbudowy jednostki dekodującej. Wiązało się to najczęściej z wykorzystaniem mikrokodu. Dekoder oparty na mikrokodzie stawał się niejako interpreterem instrukcji maszynowych, pobieranych z pamięci, i realizował je za pomocą kodu niższego poziomu. Stosowano także, dla zaoszczędzenia liczby użytych tranzystorów, kod jeszcze niższego rzędu, tzw. nano-kod, którego sekwencje wywoływane były przez mikro-kod (patrz Motorola 68000). Istniały także rozwiązania hybrydowe, łączące wykorzystanie układów logicznych (ang. random logic) z mikro-kodem (np. rodzina Zilog Z8000).

Konsekwencją wzrostu złożoności układów dekodujących było zwiększenie ilości cykli potrzebnych do zdekodowania rozkazu, co przekładało się na spadek prędkości wykonywania programu. Środkiem zaradczym, tak jak w przypadku mainframeów, było podzielenie jednostki centralnej na wiele jednostek funkcjonalnych, co miało umożliwić wykonywanie kilku operacji jednocześnie. W ten sposób np. proces obliczania adresu efektywnego mógł zachodzić równolegle z obliczaniem wyniku operacji arytmetycznej, której wynik miał być później zapisany w pamięci. W takiej architekturze mniej liczył się sam czas wykonania konkretnej instrukcji, a bardziej ilość instrukcji pobieranych w jednostce czasu. Przy przetwarzaniu potokowym wiele instrukcji mogło być wykonywanych, w całości lub częściowo, równolegle. Rozbijanie złożonych instrukcji na etapy skutkowało jeszcze większym rozbudowaniem jednostki dekodującej i logiki sterującej. Dodatkowym impulsem do rozbudowy listy rozkazów był fakt, że stosunkowo dużo kodu powstawało wówczas bezpośrednio w asemblerze. Kompilatory tamtych czasów były proste i często nie potrafiły efektywnie wykorzystać bardziej wyrafinowanych możliwości, które dawał mikroprocesor, więc z konieczności zadanie to spoczywało na programistach. Tego rodzaju błędne koło można było przerwać tylko przez radykalne uproszczenie listy rozkazów – oznaczało to jednak porzucenie zgodności z poprzednimi modelami mikroprocesorów danej rodziny. Na tak radykalny krok mało który producent mikroprocesorów mógł sobie pozwolić.

Pierwsze mikroprocesory były z konieczności proste i architektonicznie bliższe sterownikom przemysłowym niż współczesnym im minikomputerom. Inercja projektantów, pomimo rozwoju technologii, była trudna do przezwyciężenia. Dopiero konstrukcje 16-bitowe, a później kolejne, w pełni 32-bitowe, mogły zbliżyć się do swoich pierwowzorów – do procesorów trzeciej generacji.

Intel 80386 jest przykładem dojrzałej konstrukcji, na której można było zaimplementować wielodostępne, „poważne” systemy operacyjne klasy UNIX. Inaczej niż w przypadku Motoroli 68000, projektanci zachowali zgodność na poziomie kodu maszynowego z poprzednimi modelami rodziny x86. W efekcie otrzymano mikroprocesor z listą rozkazów o zmiennej długości, mogący operować na danych różnych rozmiarach. Instrukcje, które pierwotnie operowały na dedykowanych dla nich rejestrach, w kolejnych modelach zyskiwały na elastyczności. Intel 80386 ma znacznie większą ortogonalność listy rozkazów niż model i8086. Bardziej skomplikowaną do dekodowania listę rozkazów zaimplementowano na poziomie mikro-kodu. Wewnętrzna architektura mikroprocesora została rozdzielona na wiele jednostek funkcjonalnych, a kluczowe dla wydajności elementy zostały powielone, by umożliwić równoległe wykonywanie niezależnych od siebie mikroinstrukcji.

Intel 80386 może wykonywać kod maszynowy i8086 w trybie rzeczywistym, znanym z i80286, oraz trybie wirtualnym i8086 (V86). Standardowo, podobnie jak poprzednik, po resecie pracuje w trybie rzeczywistym. W tym trybie może wykonywać bezpośrednio kod i8086 z większą wydajnością niż oryginał. Tryb V86 natomiast jest rozszerzeniem, dzięki któremu zadanie pracujące w trybie chronionym uzyskuje dostęp do pierwszego megabajta pamięci i umożliwia wykonywanie programów napisanych dla 8086 w środowisku maksymalnie zbliżonym do trybu rzeczywistego. Korzystając jednak z możliwości trybu chronionego, kod wykonywany w tym trybie może być całkowicie izolowany od innych zadań. Aby dodatkowo rozdzielić przestrzenie adresowe programów pracujących w trybie V86, które współdzielą obszar pierwszego megabajta pamięci, wykorzystać można mechanizm stronicowania.

W dalszej części zostaną omówione tylko te elementy architektury i80386, które zostały dodane lub rozszerzone względem i80286. Poprzedni artykuł z tego cyklu zawiera opisy starszych modeli rodziny x86. Model dostępu do pamięci, oprócz standardowego mechanizmu segmentacji opartego na selektorach segmentów, został w i80386 wzbogacony o mechanizm stronicowania. Translację adresów logicznych na adresy fizyczne, wystawiane na szynę adresową mikroprocesora, dokonuje jego wewnętrzne MMU (ang. memory management unit – jednostka zarządzania pamięcią). Proces mapowania składa się z następujących kroków:

Adres logiczny składa się z 16-bitowego selektora segmentu oraz 32-bitowego przesunięcia (ang. offset) wewnątrz tego segmentu. W trybie rzeczywistym i V86 przesunięcie jest 16-bitowe. 20-bitowy adres fizyczny jest wyliczany przez pomnożenie 16-bitowego selektora przez 16 (przesunięcie o 4 bity w lewo) i dodanie do niego 16-bitowego przesunięcia.

W trybie chronionym selektor służy do wybrania odpowiedniego deskryptora segmentu z tablicy lokalnej (LDT) lub globalnej (GDT). W deskryptorze zdefiniowane są różne parametry segmentu, w tym jego adres bazowy oraz rozmiar. Korzystając z danych deskryptora, formowany jest 32-bitowy, płaski (liniowy) adres efektywny. Jeśli stronicowanie jest wyłączone, adres ten staje się bezpośrednio adresem fizycznym i jest podawany na szynę adresową mikroprocesora. Mechanizm stronicowania jest wykorzystywany do tworzenia logicznej przestrzeni adresowej dla zadania, wraz z określeniem praw dostępu do odpowiednich obszarów.

Jeśli mechanizm stronicowania jest włączony (ustawiony bit PE w rejestrze CR0), to uzyskany z modułu segmentacji adres liniowy jest poddawany dodatkowej translacji. Mechanizm stronicowania opiera się na podziale obszaru pamięci na bloki, tzw. strony (ang. page), o stałym rozmiarze 4 KiB. Podobnie jak w mechanizmie segmentacji, istnieją lokalne i globalne katalogi tablic stron. Każdy katalog tablic stron zawiera 1024 (210) elementów. Podobnie każda tablica stron zawiera 1024 (210) elementów wyznaczających fizyczny adres strony. Adres liniowy interpretowany jest więc jako 10-bitowy numer katalogu tablic stron, 10-bitowy indeks strony w tym katalogu oraz 12-bitowe przesunięcie w ramach strony. Daje to razem 32 bity. Mechanizm ten jest wykorzystywany głównie do implementacji pamięci wirtualnej połączonej z przenoszeniem niewykorzystywanych aktualnie stron na dysk (ang. page swapping). W przypadku działania wielu zadań w trybie V86 dzięki mechanizmowi stronicowania możliwa jest separacja ich przestrzeni adresowych.

Jak widać z powyższego, pobieżnego omówienia zintegrowane w strukturze MMU zapewnia wyrafinowane metody ochrony i wirtualizacji pamięci. Jest to wyraźny ukłon w stronę twórców wielodostępnych systemów operacyjnych.

Kolejnym tego typu mechanizmem jest, wspomagająca wielozadaniowość, koncepcja kontekstu zadania (ang. task). Dla uproszczenia można myśleć o zadaniu jak o aktualnie wykonywanym procesie, który ma przydzieloną dla siebie, zdefiniowaną w oparciu o mechanizm segmentacji i stronicowania, pamięć logiczną. Pamięć logiczna procesu może być podzielona, w sposób przezroczysty dla programu, na obszary o różnym poziomie dostępu i ochrony, który sprawdzany jest w czasie rzeczywistym przez odpowiednie mechanizmy mikroprocesora. Przy próbie odwołania się do niedozwolonego obszaru wykonania niedozwolonej operacji itd. generowany jest sprzętowy wyjątek, który nadrzędny system operacyjny powinien przechwycić i odpowiednio obsłużyć. Mechanizm wielozadaniowości (ang. multitasking) oparty jest na przełączaniu procesora na wykonywanie kolejnych zadań co pewien okres czasu. W ten sposób uzyskuje się iluzję równoległej pracy kilku procesów, pomimo że fizycznie dostępny jest tylko jeden rdzeń. Każde z zadań opisane jest przez strukturę TSS (ang. task state segment – segment stanu zadania). Zawiera ona stan wszystkich rejestrów procesora, dostępnych dla zadania, oraz adres jego lokalnej tablicy deskryptorów oraz katalogu tablic stron. W ten sposób wykonywanie może zostać wstrzymane i wznowione bez zakłóceń. Część TSS jest przeznaczona na dane „swobodne”, które system operacyjny może wykorzystać zgodnie z własnymi potrzebami, np. dla zapisania aktualnego priorytetu zadania, jego zapotrzebowania na czas procesora, listę otwartych plików itd. Rejestr TR (ang. task register – rejestr zadania) zawiera deskryptor i selektor aktualnie wykonywanego zadania.

Zadanie ma przypisany do siebie poziom uprzywilejowania, jeden z czterech dostępnych w i80386. Jest on wykorzystywany do weryfikacji uprawnień dostępu do obszarów pamięci, czy też procedur, które kod zadania może chcieć wywołać. W ten sposób kod użytkownika, pracujący z niskim poziomem uprzywilejowania, może uzyskać dostęp do zasobów innego zadania lub zasobów systemu operacyjnego wyłącznie w kontrolowany sposób. Segment stanu zadania (TSS) jest zdefiniowany jako deskryptor, podlega więc pod ogólne zasady ochrony dostępu, jakie daje system segmentacji. Osobny, uproszczony system ochrony oferuje także system stronicowania, dostępne są tam jednak tylko dwa poziomy dostępu: użytkownika i nadzorcy.

W celu wywołania przez proces użytkownika funkcji systemu operacyjnego, czyli funkcji pracujących z wyższym poziomem uprzywilejowania niż on sam, zachodzi konieczność zmiany tego poziomu na wyższy. Mechanizm pozwalający na takie przełączenie w sposób kontrolowany to tzw. bramka wywołania (ang. call gate) pozwalająca na wywołanie procedury uprzywilejowanej w standardowy sposób, pozwalając procesorowi na wykonanie wszystkich dodatkowych operacji związanych z przełączeniem kontekstu. Struktura definiująca bramkę zawiera, oprócz selektora i offsetu procedury, również minimalny poziom uprzywilejowania kodu, który może z niej skorzystać. W podobny sposób realizowane jest przełączenie kontekstu w przypadku przerwania czy wyjątku. Wykorzystywana jest wtedy odpowiednio bramka przerwania (ang. interrupt gate) lub pułapki (ang. trap gate), które definiowane są w tablicy deskryptorów przerwania IDT (ang. interrupt descriptor table). Adres IDT przechowywany jest w rejestrze IDTR.

W celu ułatwienia analizy pracy programów czy debugowania wprowadzono sprzętowe wsparcie dla obsługi pułapek instrukcji (ang. breakpoints), trybu pracy krokowej oraz pułapek wychwytujących dostęp do obszarów pamięci. Procesor może generować pułapkę po wykonaniu każdej instrukcji, po przełączeniu do określonego zadania lub po wystąpieniu warunku określonego w jednym z rejestrów sterujących debugowaniem DR0..7.

Intel 80386 - Schemat blokowy Rysunek 1. Schemat blokowy mikroprocesora Intel 80386

Jak widać, mikroprocesor Intel 80386 jest nie tylko złożony pod względem zestawu instrukcji, ale również, a może przede wszystkim, ze względu na zintegrowany układ zarządzania pamięcią i dodatkowe moduły znajdujące się w jednej strukturze z rdzeniem.

   
rok wprowadzenia do produkcji 1985
ilość tranzystorów 275000
częstotliwość taktowania 16 MHz (cykl zegarowy: 62.5 ns)
najkrótszy cykl instrukcji 125 ns (2 cykle zegarowe)
indeks prędkości 8 MIPS
dodawanie 64-bitowe 250000/s
efektywność architektury 909
typ architektury 32-bitowa, CISC
kolejność bajtów little-endian
licznik programu 16-bit segment : 32-bit offset
wskaźnik stosu 16-bit segment : 32-bit offset
rejestry ogólnego przeznaczenia 8 x 32-bit
rejestry/selektory segmentowe 6 x 16-bit
ALU 32-bit ogólnego przeznaczenia
moduły sprzętowe mnożenie/dzielenie, przesuwnik, adres efektywny
adresowanie pamięci 4 GiB (32-bit adres), segmentowe, stronicowanie pamięci
adresowanie portów 64 KiB portów wejścia/wyjścia
przerwania sprzętowe maskowalne, niemaskowalne
przerwania programowe maskowalne, priorytetowe, razem 256 (kodów)
praca z koprocesorem tak
systemy wieloprocesorowe tak

Znaczniki:

oraz w rejestrze kontrolnym CR0:

Tryby adresowania:

Tryby pracy:

Lista instrukcji została omówiona w ostatnim odcinku cyklu, przy prezentacji poprzednich mikroprocesorów rodziny x86. W i80386 rejestry ogólnego przeznaczenia zostały rozszerzone do 32-bitów oraz wprowadzono nowe, pomocnicze rejestry segmentowe. W istotny sposób zmniejszono ilość cykli potrzebnych do wykonania większości instrukcji. Sam kod procedury dodawania liczb 64-bitowych przedstawiono poniżej.

; Intel 80386 (iAPX 386)
; dodawanie dwóch liczb 64-bitowych
; ARG0, ARG1 - etykiety buforów zawierających argumenty
; wynik zapisywany jest w ARG1, dane zorganizowane są w 32-bitowe słowa
; kolejność bajtów w słowie zgodna z wymaganą przez architekturę procesora

  LEA ESI, ARG0              ; (2) ładuj adres argumentu 0 do ESI
  LEA EDI, ARG1              ; (2) ładuj adres argumentu 1 do EDI
  MOV CX, 2                  ; (2) ustaw licznik słów
  CLD                        ; (2) ustaw znacznik kierunku na inkrementację
  CLC                        ; (2) zeruj znacznik przeniesienia
NST:
  LODSD                      ; (5) ładuj blokowo kolejne słowo ARG0 do EAX
  ADC EAX,[EDI]              ; (6) dodaj kolejne słowo z ARG1 do AX
  STOSD                      ; (5) zapisz blokowo AX do kolejnego słowa ARG1
  LOOP NST                   ; (11) kontynuuj dla następnego słowa

Sumując czasy wykonania, dostajemy: 10 + 2 x 27 = 64 cykle zegara. Czas wykonania wynosi więc 4 μs, co daje 250000 dodawań na sekundę.

Reasumując, należy przyznać, że Intel 80386 był jak na owe czasy konstrukcją bardzo zaawansowaną. Jednak przez wzgląd na zachowanie zgodności z poprzednikami jego model programowy i mikroarchitektura były nadmiernie rozbudowane i niekonsekwentne. Nowoczesne rozwiązania sąsiadowały ze starymi, a nawet przestarzałymi koncepcjami rodem z mikroprocesorów 8-bitowych. Model segmentacji, przez wymóg zgodności z 16-bitowym i80286, jest niespójny, np. aby umożliwić definiowanie segmentów o większych rozmiarach, musiano dodać specjalny bit ziarnistości (ang. granularity). Cała funkcjonalność związana z implementacją trybu rzeczywistego, zgodnego z i8086 oraz tryb V86 są reliktami, obciążającymi architekturę i rdzeń niepotrzebnym balastem. Oczywiście niepotrzebnym z punktu widzenia efektywności mikroprocesora jako takiego – względy marketingowe związane z zapewnieniem kompatybilności z poprzednimi modelami bez wątpienia miały tutaj pierwszorzędne znaczenie. Zastosowanie mikroprocesora Intel 80386 w prostszych konstrukcjach, które miały być po prostu wydajne i oferować dobre parametry w jednostanowiskowych zastosowaniach, było marnowaniem jego potencjału. Trend aby oferować rozwiązania prostsze, lecz bardziej uniwersalne, zyskał na popularności, a przykład takiego właśnie podejścia zostanie zaprezentowany w dalszej części artykułu.

RISC: VTI VL86C010

Jako przykład mikroprocesora zrealizowanego w architekturze RISC (ang. reduced instruction set computer – komputer o zredukowanej liczbie rozkazów) wybrany został VLSI Technology Inc. (w skrócie VTI) model VL86C010. Jest to procesor w pełni 32-bitowy, którego struktura zawiera 10 razy mniej tranzystorów niż, omawianego powyżej, mikroprocesora Intel 80386. VL86C010 został zaprojektowany przez firmę Acorn jako układ implementujący model programowy ARMv2 oraz mikro-architekturę ARM2. Warto tutaj wspomnieć, że ARM to skrót od Acorn RISC Machine, a później Advanced RISC Machine. Początki architektury ARM to projekt brytyjskiej firmy Acorn Computers Ltd. zainspirowany architekturą 8-bitowego MOS 6502, a mający na celu stworzenie bardziej wydajnego, lecz prostego mikroprocesora o przejrzystej architekturze i uniwersalnym modelu programowym. Ścieżka ewolucji, zaprezentowana w poprzednich odcinkach cyklu, znajduje tutaj swoją kontynuację w postaci konsekwentnego dążenia do efektywnego wykorzystania zasobów.

Sama koncepcja RISC ma swoje źródło w badaniach efektywności wykorzystania listy rozkazów w poszczególnych architekturach i modelach programowych. W latach 80-tych było już jasne, że tylko niewielka część listy rozkazów, często poniżej 20%, jest najczęściej wykorzystywana w kodzie programów. W tym czasie coraz częściej programy były pisane w językach wysokiego poziomu, a coraz rzadziej w asemblerze. Kompilatory nie były jednak wystarczająco wyrafinowane, by wykorzystać złożone instrukcje oferowane przez architektury typu CISC. W efekcie najczęściej wykorzystywane były instrukcje proste i uniwersalne, które kompilator mógł łatwo wykorzystać. Projektanci architektur siłą bezwładu starali się dostarczyć jak najbardziej rozbudowany model programowy, by uczynić go komfortowym dla programisty. Pochłaniało to tysiące godzin pracy projektantów, skutkowało rozbudowaniem jednostki dekodującej rozkazy i w efekcie wymuszało wykorzystanie mikrokodu. Racjonalnym krokiem wydawało się więc dostosowanie sprzętu do rzeczywistych potrzeb programistów, szczególnie gdy stało się jasne, jak mały podzbiór listy rozkazów jest faktycznie wykorzystywany. W przypadku mikroprocesorów na które istniała już duża baza oprogramowania taki krok byłby jednak bardzo brzemienny w skutki. W takich przypadkach nie można było po prostu zerwać z kompatybilnością. Firma Intel, nie pierwszy już raz zresztą, wpadła w taką właśnie pułapkę.

Aby nie zmuszać klientów do płacenia za nieużywaną funkcjonalność, która dodatkowo pobierała sporą ilość prądu i emitowała ciepło, co dodatkowo ograniczało maksymalną częstotliwość taktowania, wiele firm zaczęło oferować uproszczone konstrukcje typu RISC. Można wyróżnić następujące, charakterystyczne cechy tego typu architektury:

Powyższy zestaw cech jest konsekwencją faktu, że rozbicie bardziej złożonego zadania na więcej prostych instrukcji jest najczęściej i tak szybsze niż wykorzystanie skomplikowanych, dedykowanych instrukcji zaimplementowanych wewnątrz procesora. Prostszy procesor wykona je szybciej dzięki wydajniejszej jednostce dekodującej i elastyczniejszej architekturze, co zapewnia mu większą przepustowość w dostępie do pamięci i większą liczbę instrukcji wykonywanych na sekundę. Przyjęte założenia okazały się słuszne, dodatkowo okazało się, że prostszy rdzeń pobiera mniej mocy w przeliczeniu na megaherc, w związku z tym może być taktowany szybszym zegarem lub może być realizowany w tańszej technologii, co również przekłada się na wzrost efektywności.

Na Rysunku 2 przedstawiono poglądowy schemat modelu programowego mikroprocesora VL86C010. Całkowita liczba 32-bitowych rejestrów to 27, jednak nie wszystkie mogą być jednocześnie wykorzystywane. Rejestry R0..R15 są w każdym trybie pracy dostępne jednocześnie i są, poza pewnymi wyjątkami, rejestrami ogólnego przeznaczenia.

VL86C010 - Schemat blokowy Rysunek 2. Schemat blokowy mikroprocesora VLSI Technology Inc. (VTI) VL86C010 reprezentującego przykład mikroarchitektury ARM2

Istnieje konwencja przypisująca niektórym rejestrom specjalne funkcje, co nie wyklucza ich użycia w dowolnym, innym celu. Tak więc:

Każdy z dostępnych trybów pracy ma swój własny podzbiór rejestrów, które zastępują niektóre rejestry trybu użytkownika. Dostępne są następujące tryby pracy:

Zapisywania adresu powrotu w rejestrze zamiast na stosie jest swego rodzaju powrotem do koncepcji stosu sprzętowego, znanego już z 4-bitowego mikroprocesora Intel 4004. W przypadku procedur obsługi przerwania daje to bardzo znaczącą redukcję czasu obsługi. Do przechowywania adresów powrotu wykorzystywany jest rejestr R14. Jako wskaźnik stosu wykorzystywany jest rejestr R13, natomiast R12 jako wskaźnik tzw. ramki stosu (ang. stack frame). W skrócie, R13 zawiera aktualny, a R12 poprzedni adres szczytu stosu. W ten sposób obszar pomiędzy R12 i R13 może być wykorzystany do alokowania tzw. zmiennych automatycznych. Są to zmienne dealokowane automatycznie po powrocie z podprogramu, ponieważ ich obszar pamięci umieszczony jest na stosie. Tego typu mechanizm alokacji jest bardzo powszechny i występuje w praktycznie każdym rodzaju procesora.

Rejestr R15 jest jedynym, który nie ma kopii i jest bezpośrednio dostępny w każdym trybie pracy. Ten 32-bitowy rejestr jest funkcjonalnym złożeniem rejestru licznika programu PC i rejestru stanu procesora PSR:

Takie połączenie w jednym, 32-bitowym rejestrze dwóch niezależnych struktur danych ma, pomimo oczywistych wad, jedną zaletę. Pozwala na zapisanie stanu i adresu powrotu mikroprocesora w jednym słowie, za pomocą jednej, standardowej instrukcji przesłania danych.

   
rok wprowadzenia do produkcji 1986
ilość tranzystorów 27000
częstotliwość taktowania 10 MHz (cykl zegarowy: 100 ns)
najkrótszy cykl instrukcji 100 ns (1 cykl zegarowy)
indeks prędkości 10 MIPS
dodawanie 64-bitowe 277 777/s
efektywność architektury 10288
typ architektury 32-bitowa, RISC, load-store
kolejność bajtów little-endian
licznik programu 24-bit licznik 32-bitowych słów
stos 32-bit wskaźnik, dowolny rejestr, zazwyczaj R13
rejestry 27 x 32-bit
ALU 32-bit ogólnego przeznaczenia
sprzętowe moduły obliczeniowe mnożenie/dzielenie, przesuwnik, adres efektywny
adresowanie pamięci 64 MiB (24-bit adres słowa 32-bitowego)
adresowanie portów mapowane w obszarze pamięci
przerwania sprzętowe maskowalne szybkie, maskowalne
przerwania programowe maskowalne, wyjątek/pułapka
obsługa wg stałych priorytetów tak
systemy wieloprocesorowe nie
   

Znaczniki:

Tryby adresowania:

tryb adresowania bezpośredniego(fizycznego) lub logicznego w zależności od trybu pracy i obecności MMU w systemie.

Tryby pracy:

Tym, co jest w przypadku tego mikroprocesora najciekawsze, a co jest charakterystyczne dla całej rodziny RISC, jest prostota i regularność listy rozkazów. Prostota i regularność umożliwia implementację dekodera instrukcji w formie sprzętowej, jako układu logicznego, zamiast dekodera opartego na mikrokodzie.

Każda instrukcja mikroprocesora VL86C010 ma 32-bity; pozwala to uprościć układ pobierania i kolejkowania instrukcji oraz pobierania z wyprzedzeniem. Nie jest potrzebna informacja z układu dekodującego o rozmiarze instrukcji do pobrania, ponieważ jest on stały. Kolejną generalizacją jest umieszczenie w kodzie każdej instrukcji 4-bitowego kodu warunkowego. Bity 28..31 każdej instrukcji określają warunek, który musi wystąpić, aby była ona wykonana przez procesor. Jeśli warunek nie wystąpi, instrukcja jest ignorowana, a przetwarzanie przechodzi do następnej. Kody binarne warunków wraz z opisami podane są poniżej:

kod skrót opis znaczniki
0000 EQ (ang. equal) równe Z=1
0001 NE (ang. not equal) różne Z=0
0010 CS (ang. carry set) przeniesienie C=1
0011 CC (ang. carry clear) brak przeniesienia C=0
0100 MI (ang. minus) mniejsze od zera N=1
0101 PL (ang. plus) nie mniejsze od zera N=0
0110 VS (ang. overflow set) przepełnienie V=1
0111 VC (ang. overflow clear) brak przepełnienia V=0
1000 HI (ang. unsigned higher) większe (bez znaku) C=1,Z=0
1001 LS (ang. unsigned less) nie większe (bez znaku) C=0,Z=1
1010 GE (ang. greater or equal) większe lub równe N=V
1011 LT (ang. less than) mniejsze N≠V
1100 GT (ang. greater than) większe Z=0 i N=V
1101 LE (ang. less or equal) mniejsze lub równe Z=0 lub N≠V
1110 AL (ang. always) zawsze  
1111 NV (ang. never) nigdy  
       

Podając warunek NV, każda instrukcja staje się instrukcją NOP znaną z innych modeli programowych.

Zestaw instrukcji zasadniczo można podzielić na następujące kategorie:

Jak widać na przykładzie umieszczenia warunku wykonania w kodzie każdej instrukcji, programowanie mikroprocesora typu RISC jest trochę podobne do programowania w mikrokodzie. Każda instrukcja ma 32-bity, co jest w wielu przypadkach wielkością o wiele za dużą, jednak dzięki temu układ dekodujący może być szybki i zaimplementowany sprzętowo. VL86C010 jest mikroprocesorem o architekturze load-store, w którym istotną rolę pełnią dostępne przy transferach między rejestrami i pamięcią tryby adresowania. Lista instrukcji przesyłania danych została zebrana w poniższej tabeli.

Instrukcja Opis
LDR załaduj do rejestru wartość z pamięci
STR zapisz zawartość rejestru do pamięci
LDM załaduj do listy rejestrów wartości z kolejnych komórek pamięci
SRM zapisz listę rejestrów do kolejnych komórek pamięci
   

Jednym z argumentów jest zawsze rejestr (lub rejestry) wewnętrzny, drugim zaś adres efektywny. Sposób wyliczania adresu efektywnego zależy od wybranego trybu adresowania:

Wartość offsetu lub rejestru indeksowego może być przesunięta bitowo i dopiero potem wykorzystana do wyliczenia adresu. Daje to możliwość skalowania kroku np. przy dostępie do tablic w trybie adresowania indeksowego. Przesunięcie może być wartością dodatnią lub ujemną, steruje tym osobny bit w kodzie instrukcji. Instrukcje przesyłające dane mogą operować na pojedynczych bajtach lub na 32-bitowych słowach, jest to również definiowane przez osobny bit. W przypadku przesyłania danych o rozmiarze bajtu możliwe jest wykorzystanie adresów niewyrównanych do pełnych 4-bajtów. Transfery blokowe, obejmujące kilka rejestrów, mają nieco okrojony zbiór dostępnych trybów adresowania. W trakcie takiego transferu mogą również wystąpić wyjątki związane z dostępem do zabronionego obszaru pamięci.

Kolejną grupą rozkazów są rozkazy przetwarzające dane. W przypadku tego mikroprocesora zalicza się do nich także instrukcje transferu między rejestrami. Wszystkie instrukcje tej kategorii zebrane zostały w poniższej tabeli.

instrukcja opis
ADC dodawanie z przeniesieniem
ADD dodawanie
AND iloczyn logiczny
BIC skasuj bit
CMN porównanie oparte na sumie argumentów
CMP porównanie oparte na różnicy argumentów (standardowe)
EOR suma modulo 2, czyli XOR
MLA mnożenie z akumulacją wyniku
MUL mnożenie
MOV transfer danych między rejestrami
MVN transfer danych między rejestrami z negacją
ORR suma logiczna, OR
RSB odejmowanie z odwróceniem kolejności argumentów
SUB odejmowanie
RSC odejmowanie z odwróceniem kolejności argumentów i przeniesieniem
SBC odejmowanie z przeniesieniem
TEQ test na równość argumentów (XOR bez zapisania wyniku)
TST test z wykorzystaniem maski bitowej
   

Zestaw instrukcji jest prosty, lecz w połączeniu z elastycznością w definiowaniu drugiego argumentu oraz możliwością zastosowania do niego przesunięcia daje wszechstronne i elastyczne narzędzie dla programisty czy kompilatora. Do drugiego argumentu instrukcji przetwarzającej dane można zastosować przesunięcie o określoną ilość bitów. Wartość przesunięcia może być zdefiniowana jako literał lub może być pobrana z innego rejestru. Do wyboru są tryby przesunięcia w prawo, w lewo, z wykorzystaniem znacznika C lub wartości 1. Możliwy jest również obrót z wykorzystaniem znacznika C jako dodatkowego bitu.

W niniejszym artykule nie ma miejsca na systematyczny wykład assemblera mikroprocesora VL86C010, w tym miejscu autor odsyła do stosownej literatury. Ogólny styl i możliwości, jakie daje architektura ARMv2, zaprezentowane zostały na poniższych, wybranych fragmentach kodu.

Przykład 1:

TEQ		R1, 0
RSBMI	R1, R1, 0

Sprawdź (TEQ), czy R1 ma wartość 0. Instrukcja ustawia również znacznik znaku N. Następnie wykonaj odejmowanie z odwróceniem kolejności operandów (RSB), tylko jeśli testowanie wykryło wartość ujemną (MI). Wynik operacji 0 - R1 wpisz do rejestru R1. Biorąc pod uwagę, że przy odejmowaniu R1 musi być wartością ujemną, cały powyższy kod przekształca liczbę zapisaną w R1 na jej wartość absolutną. Możliwość zadeklarowania dowolnej instrukcji jako wykonywanej warunkowo, dodając do niej odpowiedni postfix, pokazuje w tym przykładzie swoją użyteczność.

Przykład 2:

ADD		R1, R1, R1 LSL 1
MOV		R1, R1 LSL 1

Pierwsza instrukcja dodaje (ADD) wartość rejestru R1 do wartości rejestru R1 przesuniętej w lewo (LSL) o jeden bit. W efekcie mamy R1 + 2*R1 = 3*R1 i taka wartość zostaje wpisana do R1. Następnie prześlij (MOV) do rejestru R1 wartość R1 przesuniętą w lewo (LSL) o jeden bit, czyli pomnożoną przez 2. Wartość w rejestrze R1 zostaje pomnożona najpierw przez 3, a potem przez 2, czyli w efekcie przez 6. Za pomocą dwóch instrukcji, z wykorzystaniem sprzętowego przesuwnika bitowego można zrealizować błyskawiczne mnożenie przez 6.

Przykład 3: LDR R1, [R0, 8] STR R1, [R2, R3 LSL 2]!

Załaduj rejestr (LDR), w tym przypadku R1 wartością z komórki pamięci o adresie zapisanym w rejestrze R0 z przesunięciem 8. Nie modyfikuj wartości rejestru R0. Następnie zapisz tak pobraną wartość rejestru (STR) R1 do komórki pamięci o adresie zapisanym w rejestrze R2 powiększoną o wartość rejestru R3 przesuniętą uprzednio w lewo (LSL) o 2 bity. Po wykonaniu instrukcji zapisz (!) tak wyliczony adres efektywny do rejestru bazowego R2. Powyższe instrukcje pokazują elastyczność i siłę wyrazu modelu programowego opartego na łączeniu funkcji podstawowych bloków mikroarchitektury w jednej instrukcji.

Struktura asemblera ARMv2 jest nieco podobna w swojej regularności do asemblera mikroprocesora Motorola 68000, który został opisany w poprzednim odcinku cyklu. Separacja instrukcji przetwarzających dane od instrukcji przesyłających daje czysty i elegancki kod. Pomimo że często musi zawierać więcej operacji, jest dzięki swojej prostocie wykonywany szybciej. Widać to na przykładzie podanego niżej, standardowego programu testowego. Widoczna niedogodność to konieczność zapisywania i odtwarzania znacznika przeniesienia między kolejnymi operacjami dodawania z powodu braku np. dedykowanej instrukcji skoku po odliczeniu do zera.

; VLSI Technology Inc. VL86C010 (ARMv2)
; dodawanie dwóch liczb 64-bitowych
; ARG0, ARG1 - etykiety buforów zawierających argumenty
; wynik zapisywany jest w ARG1, dane zorganizowane są w 32-bitowe słowa
; kolejność bajtów w słowie zgodna z wymaganą przez architekturę procesora

  LEA  R0, ARG0                  ; (2) ładuj adres argumentu 0 do R0
  LEA  R1, ARG1                  ; (2) ładuj adres argumentu 1 do R1
  ADD  R2, R1, 8                 ; (1) ładuj adres po końcu bufora argumentu 1
  MOV  R5, 0                     ; (1) zeruj bufor znacznika C (rejestr R5)
NST:
  LDR  R3, [R0],4                ; (3) ładuj kolejne słowo ARG0 do R3, R0 = R0+4
  LDR  R4, [R1]                  ; (3) ładuj kolejne słowo ARG1 do R4
  MOVS R5, R5 LSL 1              ; (1) odtwórz znacznik C z MSB rejestru R5
  ADCS R4, R3, R4                ; (1) dodaj słowa ARG0 i ARG1 wynik zapisz w R4
  STR  R4, [R1],4                ; (2) zapisz R4 do słowa ARG1, R1 = R1+4
  MOVS R5, R5 RRX 1              ; (1) zapisz znacznik C w MSB rejestru R5
  CMP  R1, R2                    ; (1) sprawdź, czy już dodano wszystkie słowa
  BNE  NST                       ; (3) kontynuuj, jeśli nie

Sumując czasy wykonania, dostajemy: 6 + 2 x 15 = 36 cykli zegara. Czas wykonania wynosi więc 3.6 μs, co daje 277 777 dodawań na sekundę. Obliczając, zgodnie z przyjętą metodologią, wskaźnik efektywności, dostajemy wartość ponad 10-krotnie większą niż uzyskał Intel 80386. Tak miażdżąca przewaga wynika z faktu, że w przypadku VL86C010 płacimy tylko za to, czego potrzebujemy w tak prostym teście. Wskaźnik efektywności dla prostych zadań oddaje ten fakt z bezlitosną precyzją.

Kolejną, nie omówioną wcześniej klasą instrukcji są operacje komunikacji z koprocesorem. Do sterowania komunikacją służa wyprowadzone na zewnątrz linie:

Wejście CPA (ang. co-processor absent – koprocesor nieobecny) – jeśli koprocesor nie jest fizycznie zainstalowany, nie będzie w stanie zasygnalizować swojej obecności po wykryciu przeznaczonej dla niego instrukcji. W takim przypadku wymiana potwierdzeń (ang. handshake) między procesorem a koprocesorem jest przerywana i wywoływany jest wyjątek/pułapka związany z niedozwoloną instrukcją. W przeciwnym razie procesor główny aktywuje linię CPI i oczekuje na zakończenie wykonywania instrukcji, testując linię CPB.

Wejście CPB (ang. co-processor busy – koprocesor zajęty) – koprocesor aktywuje tę linię podczas przetwarzania przeznaczonej dla niego instrukcji. Daje to sygnał procesorowi do wstrzymania wykonywania aż do jej zakończenia. Koprocesor powinien aktywować sygnał dopiero w momencie, gdy jest gotów na odbieranie danych związanych z konkretną instrukcją. Procesor główny jest wstrzymywany, by dać czas koprocesorowi na przygotowanie.

Wyjście CPI (ang. co-processor instruction – instrukcja koprocesora) – procesor główny aktywuje tę linię podczas przetwarzania instrukcji przez koprocesor. W tym czasie program jest wstrzymywany, a dalsze przetwarzanie uzależnione od stanu linii CPA i CPB.

Instrukcje służące do komunikacji z koprocesorem:

Jak widać, architektura load-store ma swoje odzwierciedlenie również w obszarze komunikacji z koprocesorem.

Podsumowanie

Przez wszystkie poprzednie odcinki cyklu o ewolucji mikroprocesorów obserwowaliśmy zmaganie się ze sobą dwóch paradygmatów. Terminy RISC i CISC oddają podejście projektantów poszczególnych architektur i modeli programowych na tyle dobrze, że możemy w końcu w miarę precyzyjnie nazwać te dwa odmienne trendy. Mikroprocesory RISC, jako prostsze, a więc tańsze i bardziej energooszczędne zdominowały dosyć szybko segment urządzeń mobilnych, czyli ten obszar, w którym wydajność nie mogła być osiągana kosztem dużego poboru mocy. Zdobywanie doświadczenia w tworzeniu efektywnych konstrukcji opłaciło się, gdy pojawiła się technologiczna bariera dla zwiększania częstotliwości taktowania. Mikroprocesory RISC, pozbawione bagażu kompatybilności wstecznej i przerośniętej listy rozkazów, pokazały swoją przewagę w sytuacji, gdy „surowa moc” nie była już dla architektur CISC dostępna bez ograniczeń. W chwili pisania tego artykułu znajdujemy się w przededniu pierwszego szturmu na bastion CISC, w którym nowy mikroprocesor RISC firmy Apple ma wyprzeć z komputerów tej firmy zadomowione tam od lat mikroprocesory firmy Intel.

Źródła i bibliografia

W powstaniu niniejszego artykułu wykorzystane zostały następujące publikacje i źródła: