Jeśli rejestry są tak niesamowicie szybkie, dlaczego nie mamy ich więcej?

88

W wersji 32-bitowej mieliśmy 8 rejestrów „ogólnego przeznaczenia”. W przypadku wersji 64-bitowej kwota podwaja się, ale wydaje się niezależna od samej zmiany 64-bitowej.
Skoro rejestry są tak szybkie (brak dostępu do pamięci), dlaczego nie ma ich więcej naturalnie? Czy konstruktorzy procesorów nie powinni umieszczać w procesorze jak największej liczby rejestrów? Jakie jest logiczne ograniczenie, dlaczego mamy tylko tyle, ile mamy?

Xeo
źródło
Procesory i procesory graficzne ukrywają opóźnienia głównie za pomocą odpowiednio pamięci podręcznych i masowej wielowątkowości. Tak więc procesory mają (lub potrzebują) niewiele rejestrów, podczas gdy procesory graficzne mają dziesiątki tysięcy rejestrów. Zobacz moją ankietę dotyczącą pliku rejestru GPU, w której omówiono wszystkie te kompromisy i czynniki.
user984260

Odpowiedzi:

119

Jest wiele powodów, dla których nie masz tylko dużej liczby rejestrów:

  • Są silnie powiązane z większością etapów rurociągu. Na początek musisz śledzić ich żywotność i przesuwać wyniki z powrotem do poprzednich etapów. Złożoność staje się trudna do rozwiązania bardzo szybko, a liczba zaangażowanych drutów (dosłownie) rośnie w tym samym tempie. Jest drogie pod względem powierzchni, co ostatecznie oznacza, że ​​po pewnym czasie jest drogie pod względem mocy, ceny i wydajności.
  • Zajmuje miejsce na kodowanie instrukcji. 16 rejestrów zajmuje 4 bity dla źródła i celu, a kolejne 4, jeśli masz instrukcje 3-operandowe (np. ARM). To bardzo dużo miejsca na kodowanie zestawu instrukcji, które zajmuje tylko określenie rejestru. To ostatecznie wpływa na dekodowanie, rozmiar kodu i ponownie złożoność.
  • Są lepsze sposoby na osiągnięcie tego samego rezultatu ...

Obecnie naprawdę mamy dużo rejestrów - po prostu nie są one jawnie zaprogramowane. Mamy "zmianę nazwy rejestru". Chociaż masz dostęp tylko do małego zestawu (8-32 rejestrów), w rzeczywistości są one obsługiwane przez znacznie większy zestaw (np. 64-256). Następnie CPU śledzi widoczność każdego rejestru i przydziela je do zestawu o zmienionej nazwie. Na przykład, możesz ładować, modyfikować, a następnie przechowywać w rejestrze wiele razy z rzędu i mieć każdą z tych operacji faktycznie wykonywanych niezależnie w zależności od błędów pamięci podręcznej itp. W ARM:

ldr r0, [r4]
add r0, r0, #1
str r0, [r4]
ldr r0, [r5]
add r0, r0, #1
str r0, [r5]

Rdzenie Cortex A9 zmieniają nazwy rejestrów, więc pierwsze ładowanie do "r0" trafia do wirtualnego rejestru o zmienionej nazwie - nazwijmy go "v0". Ładowanie, zwiększanie i zapisywanie odbywa się na „v0”. W międzyczasie ponownie wykonujemy ładowanie / modyfikowanie / zapisywanie do r0, ale nazwa zostanie zmieniona na „v1”, ponieważ jest to całkowicie niezależna sekwencja wykorzystująca r0. Powiedzmy, że ładowanie ze wskaźnika w "r4" utknęło z powodu braku pamięci podręcznej. W porządku - nie musimy czekać, aż "r0" będzie gotowe. Ponieważ ma zmienioną nazwę, możemy uruchomić następną sekwencję z "v1" (również odwzorowanym na r0) - i być może jest to trafienie w pamięć podręczną i właśnie odnieśliśmy ogromną wygraną w wydajności.

ldr v0, [v2]
add v0, v0, #1
str v0, [v2]
ldr v1, [v3]
add v1, v1, #1
str v1, [v3]

Myślę, że x86 ma obecnie do gigantycznej liczby zmienionych nazw rejestrów (ballpark 256). Oznaczałoby to posiadanie 8 bitów razy 2 dla każdej instrukcji tylko po to, aby powiedzieć, jakie jest źródło i cel. Zwiększyłoby to znacznie liczbę przewodów potrzebnych w rdzeniu i jego rozmiar. Tak więc istnieje dobre miejsce w okolicach 16-32 rejestrów, na które zdecydowała się większość projektantów, aw przypadku niedziałających projektów procesorów zmiana nazwy rejestrów jest sposobem na złagodzenie tego problemu.

Edycja : znaczenie wykonywania poza kolejnością i zmiany nazwy rejestru w tym. Gdy masz OOO, liczba rejestrów nie ma tak dużego znaczenia, ponieważ są to tylko „tymczasowe znaczniki”, których nazwa zostaje zmieniona na znacznie większy zestaw rejestrów wirtualnych. Nie chcesz, aby liczba była zbyt mała, ponieważ pisanie małych sekwencji kodu jest trudne. Jest to problem dla x86-32, ponieważ ograniczone 8 rejestrów oznacza, że ​​wiele danych tymczasowych przechodzi przez stos, a rdzeń potrzebuje dodatkowej logiki, aby przekazywać odczyty / zapisy do pamięci. Jeśli nie masz OOO, zwykle mówisz o małym rdzeniu, w którym to przypadku duży zestaw rejestrów to słaba korzyść koszt / wydajność.

Jest więc naturalny punkt optymalny dla rozmiaru banku rejestrów, który wynosi maksymalnie około 32 zaprojektowanych rejestrów dla większości klas procesorów. x86-32 ma 8 rejestrów i jest zdecydowanie za mały. ARM poszedł z 16 rejestrami i to dobry kompromis. 32 rejestry to trochę za dużo, jeśli w ogóle - w końcu nie potrzebujesz ostatnich 10 lub więcej.

Nic z tego nie dotyczy dodatkowych rejestrów, które otrzymujesz dla SSE i innych koprocesorów zmiennoprzecinkowych wektorów. Mają sens jako dodatkowy zestaw, ponieważ działają niezależnie od rdzenia typu integer i nie zwiększają wykładniczo złożoności procesora.

John Ripley
źródło
12
Doskonała odpowiedź - chciałbym wrzucić kolejny powód do tego miksu - im więcej masz rejestrów, tym więcej czasu zajmuje wrzucenie ich do / wyciągnięcie ze stosu podczas przełączania kontekstu. Zdecydowanie nie jest to główna kwestia, ale do rozważenia.
Will A
7
@WillA dobra uwaga. Jednak architektury z wieloma rejestrami mają sposoby na złagodzenie tego kosztu. ABI zwykle ma zapis wywoływany większości rejestrów, więc wystarczy zapisać zestaw podstawowy. Przełączanie kontekstu jest zwykle na tyle kosztowne, że dodatkowe składowanie / przywracanie nie kosztuje dużo w porównaniu z wszystkimi innymi formalnościami. SPARC faktycznie działa w tym zakresie, czyniąc bank rejestrów „oknem” w obszarze pamięci, więc trochę się skaluje (coś w rodzaju machania ręką).
John Ripley
4
Zastanów się, czy mój umysł poruszyła tak dokładna odpowiedź, której na pewno się nie spodziewałem. Dzięki za wyjaśnienie, dlaczego tak naprawdę nie potrzebujemy tylu nazwanych rejestrów, to bardzo interesujące! Naprawdę podobało mi się przeczytanie Twojej odpowiedzi, ponieważ jestem całkowicie zainteresowany tym, co dzieje się „pod maską”. :) Poczekam trochę dłużej, zanim zaakceptuję odpowiedź, bo nigdy nie wiadomo, ale moje +1 jest pewne.
Xeo
1
niezależnie od tego, gdzie spoczywa odpowiedzialność za zapisywanie rejestrów, czas, jaki zajmuje, stanowi obciążenie administracyjne. OK, więc przełączanie kontekstu może nie być najczęściej występującym przypadkiem, ale przerwania tak. Ręczne procedury mogą oszczędzać na rejestrach, ale jeśli sterowniki są napisane w C, jest szansa, że ​​funkcja zadeklarowana jako przerwanie zapisze każdy pojedynczy rejestr, wywoła isr, a następnie przywróci wszystkie zapisane rejestry. IA-32 miał przewagę przerwań z jego 15-20 regs w porównaniu do 32 + coś w regach architektur RISC.
Olof Forshell
1
Doskonała odpowiedź, ale nie zgodzę się z bezpośrednim porównaniem rejestrów „przemianowanych” z rejestrami „prawdziwymi” adresowalnymi. Na x86-32, nawet przy 256 wewnętrznych rejestrach, nie można użyć więcej niż 8 tymczasowych wartości przechowywanych w rejestrach w żadnym pojedynczym punkcie wykonania. Zasadniczo zmiana nazwy rejestru jest tylko ciekawym produktem ubocznym OOE, nic więcej.
noop
12

Mamy Czy ich więcej

Ponieważ prawie każda instrukcja musi wybierać 1, 2 lub 3 architektonicznie widoczne rejestry, zwiększenie ich liczby zwiększyłoby rozmiar kodu o kilka bitów w każdej instrukcji, a tym samym zmniejszyłoby gęstość kodu. Zwiększa również ilość kontekstu, który należy zapisać jako stan wątku i częściowo zapisać w rekordzie aktywacji funkcji . Operacje te występują często. Blokady rurociągów muszą sprawdzać tablicę wyników dla każdego rejestru, a to ma kwadratową złożoność czasową i przestrzenną. I chyba największym powodem jest po prostu zgodność z już zdefiniowanym zestawem instrukcji.

Ale okazuje się, dzięki przemianowanie rejestrów , tak naprawdę nie mają wiele dostępnych rejestrów, a my nawet nie trzeba, aby je zapisać. Procesor w rzeczywistości ma wiele zestawów rejestrów i automatycznie przełącza się między nimi w miarę wykonywania kodu. Robi to wyłącznie po to, aby uzyskać więcej rejestrów.

Przykład:

load  r1, a  # x = a
store r1, x
load  r1, b  # y = b
store r1, y

W architekturze, która ma tylko r0-r7, następujący kod może zostać automatycznie przepisany przez procesor jako coś takiego:

load  r1, a
store r1, x
load  r10, b
store r10, y

W tym przypadku r10 jest ukrytym rejestrem, który tymczasowo zastępuje r1. CPU może stwierdzić, że wartość r1 nigdy nie jest używana ponownie po pierwszym zapisie. Pozwala to na opóźnienie pierwszego obciążenia (nawet trafienie w pamięć podręczną na chipie zajmuje zwykle kilka cykli) bez konieczności opóźnienia drugiego obciążenia lub drugiego magazynu.

DigitalRoss
źródło
2

Dodają rejestry przez cały czas, ale często są powiązane z instrukcjami specjalnego przeznaczenia (np. SIMD, SSE2 itp.) Lub wymagają kompilacji do określonej architektury procesora, co zmniejsza przenośność. Istniejące instrukcje często działają na określonych rejestrach i nie mogą korzystać z innych rejestrów, gdyby były dostępne. Starszy zestaw instrukcji i wszystko.

Seth Robertson
źródło
1

Aby dodać tutaj trochę interesujących informacji, zauważysz, że posiadanie 8 rejestrów tej samej wielkości pozwala kodom operacyjnym zachować spójność z notacją szesnastkową. Na przykład instrukcja push axto opcode 0x50 na x86 i dochodzi do 0x57 dla ostatniego rejestru di. Następnie instrukcja pop axzaczyna się od 0x58 i przechodzi w górę do 0x5F, pop diaby ukończyć pierwszą podstawę-16. Spójność szesnastkowa jest utrzymywana przy 8 rejestrach na rozmiar.


źródło
2
Na x86 / 64 prefiksy instrukcji REX rozszerzają indeksy rejestrów o więcej bitów.
Alexey Frunze