Dlaczego instrukcje x86-64 w rejestrach 32-bitowych zerują górną część pełnego rejestru 64-bitowego?

119

W x86-64 Tour Intel Podręczniki , czytam

Być może najbardziej zaskakującym faktem jest to, że instrukcja taka jak MOV EAX, EBXautomatycznie zeruje górne 32 bity RAXrejestru.

Dokumentacja firmy Intel (3.4.1.1 Rejestry ogólnego przeznaczenia w trybie 64-bitowym w ręcznej architekturze podstawowej) cytowana w tym samym źródle mówi nam:

  • 64-bitowe operandy generują wynik 64-bitowy w docelowym rejestrze ogólnego przeznaczenia.
  • 32-bitowe operandy generują wynik 32-bitowy, rozszerzony przez zero do wyniku 64-bitowego w docelowym rejestrze ogólnego przeznaczenia.
  • 8-bitowe i 16-bitowe operandy generują wynik 8-bitowy lub 16-bitowy. Górne 56 bitów lub 48 bitów (odpowiednio) docelowego rejestru ogólnego przeznaczenia nie jest modyfikowanych przez operację. Jeśli wynik operacji 8-bitowej lub 16-bitowej ma na celu obliczenie adresu 64-bitowego, jawnie wpisz znak-rozszerz rejestr do pełnych 64-bitów.

W zestawie x86-32 i x86-64 instrukcje 16-bitowe, takie jak

mov ax, bx

nie pokazuj tego rodzaju „dziwnego” zachowania, gdy górne słowo eax jest zerowane.

A zatem: jaki jest powód, dla którego wprowadzono to zachowanie? Na pierwszy rzut oka wydaje się to nielogiczne (ale powodem może być to, że jestem przyzwyczajony do dziwactw asemblera x86-32).

Nubok
źródło
16
Jeśli wyszukasz w Google hasło „Częściowe przeciągnięcie rejestru”, znajdziesz sporo informacji o problemie, którego (prawie na pewno) próbowali uniknąć.
Jerry Coffin
4
Nie tylko „większość”. AFAIK, wszystkie instrukcje z r32operandem docelowym zerują wysoki 32, zamiast scalać. Na przykład, niektórzy monterzy zastąpi pmovmskb r64, xmmz pmovmskb r32, xmm, oszczędzając REX, ponieważ wersja 64bit przeznaczenia zachowuje się identycznie. Mimo że sekcja Operacja podręcznika zawiera osobno wszystkie 6 kombinacji źródła 32/64-bitowego dest i 64/128 / 256b, niejawne rozszerzenie zerowe postaci r32 powiela jawne rozszerzenie zerowe postaci r64. Jestem ciekawy implementacji HW ...
Peter Cordes
2
@HansPassant, zaczyna się odwołanie cykliczne.
kchoi

Odpowiedzi:

98

Nie jestem AMD ani nie mówię w ich imieniu, ale zrobiłbym to w ten sam sposób. Ponieważ wyzerowanie górnej połowy nie tworzy zależności od poprzedniej wartości, procesor musiałby czekać. Przemianowanie rejestrów mechanizm będzie zasadniczo zostać pokonany, jeśli nie została wykonana w ten sposób.

W ten sposób możesz pisać szybki kod przy użyciu wartości 32-bitowych w trybie 64-bitowym bez konieczności jawnego zrywania przez cały czas zależności. Bez tego zachowania każda pojedyncza 32-bitowa instrukcja w trybie 64-bitowym musiałaby czekać na coś, co wydarzyło się wcześniej, mimo że ta wysoka część prawie nigdy nie byłaby używana. (Tworzenie wersji int64-bitowej zmarnuje miejsce w pamięci podręcznej i przepustowość pamięci; x86-64 najefektywniej obsługuje 32- i 64-bitowe rozmiary operandów )

Zachowanie dla rozmiarów operandów 8 i 16-bitowych jest dziwne. Szaleństwo zależności jest jednym z powodów, dla których obecnie unika się 16-bitowych instrukcji. x86-64 odziedziczył to po 8086 dla 8-bitów i 386 dla 16-bitów i zdecydował, że 8 i 16-bitowe rejestry działają tak samo w trybie 64-bitowym, jak w trybie 32-bitowym.


Zobacz także Dlaczego GCC nie używa rejestrów częściowych? praktyczne szczegóły, jak zapisy do 8 i 16-bitowych rejestrów częściowych (i późniejsze odczyty pełnego rejestru) są obsługiwane przez rzeczywiste procesory.

harold
źródło
8
Nie sądzę, żeby to było dziwne, myślę, że nie chcieli zbytnio się łamać i trzymali tam stare zachowanie.
Alexey Frunze
5
@Alex, kiedy wprowadzili tryb 32-bitowy, nie było starego zachowania dla wysokiej części. Przedtem nie było żadnej wysokiej części… Oczywiście po tym nie można było już tego zmienić.
harold
1
Mówiłem o 16-bitowych operandach, dlaczego w takim przypadku górne bity nie są zerowane. Nie działają w trybach innych niż 64-bitowe. I to też jest utrzymywane w trybie 64-bitowym.
Alexey Frunze
3
Zinterpretowałem twoje „Zachowanie dla instrukcji 16-bitowych jest dziwne”, ponieważ „dziwne jest, że rozszerzenie zerowe nie występuje w przypadku 16-bitowych operandów w trybie 64-bitowym”. Stąd moje komentarze na temat utrzymania tego samego w trybie 64-bitowym dla lepszej kompatybilności.
Alexey Frunze
8
@Alex oh, rozumiem. Dobrze. Z tej perspektywy nie wydaje mi się to dziwne. Tylko z perspektywy „spojrzenia wstecz, może to nie był taki dobry pomysł” - z perspektywy. Chyba powinienem był być wyraźniejszy :)
harold
9

Po prostu oszczędza miejsce w instrukcjach i zestawie instrukcji. Możesz przenieść małe wartości natychmiastowe do rejestru 64-bitowego, korzystając z istniejących (32-bitowych) instrukcji.

Oszczędza to również konieczności kodowania 8-bajtowych wartości MOV RAX, 42, kiedy MOV EAX, 42można je ponownie wykorzystać.

Ta optymalizacja nie jest tak ważna dla operacji 8- i 16-bitowych (ponieważ są one mniejsze), a zmiana tamtejszych reguł również zepsułaby stary kod.

Bo Persson
źródło
7
Jeśli to prawda, czy nie miałoby większego sensu, gdyby rozszerzenie znaku zamiast rozszerzenia było zerowe?
Damien_The_Unbeliever,
16
Rozszerzenie znaku jest wolniejsze, nawet sprzętowo. Zerowe wydłużenie może być wykonane równolegle z każdym obliczeniem, który daje dolną połowę, ale rozszerzenie znaku nie może być wykonane, dopóki (przynajmniej znak) dolnej połowy nie zostanie obliczony.
Jerry Coffin
13
Inną powiązaną sztuczką jest użycie, XOR EAX, EAXponieważ XOR RAX, RAXpotrzebowałby prefiksu REX.
Neil
3
@Nubok: Jasne, mogli dodać kodowanie movzx / movsx, które pobiera natychmiastowy argument. Przez większość czasu jest to bardziej wygodne, aby górne bity wyzerowane, więc można użyć jako wartości indeksu tablicy (bo wszystkie regs muszą być tej samej wielkości w skutecznej adresem: [rsi + edx]nie jest to dozwolone). Oczywiście unikanie fałszywych zależności / opóźnień w rejestrach częściowych (druga odpowiedź) jest kolejnym ważnym powodem.
Peter Cordes
4
a zmiana tamtejszych zasad spowodowałaby również złamanie starego kodu. Stary kod i tak nie może działać w trybie 64-bitowym (np. 1-bajtowe inc / dec to prefiksy REX); to nie ma znaczenia. Powodem, dla którego nie usuwamy brodawek na x86, jest mniej różnic między trybami długimi i kompatybilnymi / starszymi, więc mniej instrukcji musi być dekodowanych inaczej w zależności od trybu. AMD nie wiedziało, że AMD64 się przyjmie i niestety było bardzo konserwatywne, więc obsługa wymagałaby mniejszej liczby tranzystorów. Na dłuższą metę byłoby dobrze, gdyby kompilatory i ludzie musieli pamiętać, które rzeczy działają inaczej w trybie 64-bitowym.
Peter Cordes
1

Bez zera rozszerzającego się do 64 bitów oznaczałoby to, że instrukcja odczytująca z raxmiałaby 2 zależności dla swojego raxoperandu (instrukcja, która zapisuje do eaxi instrukcja, która zapisuje raxprzed nią), to znaczy, że 1) ROB musiałby mieć wpisy dla wiele zależności dla pojedynczego operandu, co oznacza, że ​​ROB wymagałby więcej logiki i tranzystorów oraz zajmowałby więcej miejsca, a wykonanie byłoby wolniejsze, czekając na niepotrzebną drugą zależność, której wykonanie może zająć wieki; lub alternatywnie 2), co, jak przypuszczam, dzieje się z instrukcjami 16-bitowymi, etap alokacji prawdopodobnie zatrzymuje się (tj. jeśli RAT ma aktywną alokację do axzapisu i eaxpojawia się odczyt, zatrzymuje się do momentu axwycofania zapisu).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

Jedyną korzyścią wynikającą z niezerowania rozszerzania jest zapewnienie uwzględnienia bitów wyższego rzędu rax, na przykład, jeśli pierwotnie zawiera on 0xffffffffffffffff, wynikiem będzie 0xffffffff00000007, ale nie ma bardzo niewielkiego powodu, aby ISA zapewniał tę gwarancję takim kosztem, i bardziej prawdopodobne jest, że korzyść z zerowego rozszerzenia byłaby faktycznie wymagana więcej, więc oszczędza dodatkową linię kodu mov rax, 0. Gwarantując, że zawsze będzie on rozszerzony do 64 bitów, kompilatory mogą pracować z tym aksjomatem, gdy są w mov rdx, rax, raxmuszą tylko czekać na jego pojedynczą zależność, co oznacza, że ​​może rozpocząć wykonywanie szybciej i wycofać się, zwalniając jednostki wykonawcze. Co więcej, pozwala również na bardziej wydajne idiomy zerowe, takie jak xor eax, eaxzero, raxbez konieczności stosowania bajtu REX.

Lewis Kelsey
źródło
Częściowe flagi w Skylake działają przynajmniej dzięki oddzielnym wejściom dla CF w porównaniu z dowolnym SPAZO. (Więc cmovbejest 2 uops, ale cmovbjest 1). Ale żaden procesor, który dokonuje jakiejkolwiek częściowej zmiany nazwy rejestru, nie robi tego tak, jak sugerujesz. Zamiast tego wstawiają scalające uop, jeśli nazwa częściowego rejestru jest zmieniana niezależnie od pełnego rejestru (tj. Jest „brudny”). Zobacz Dlaczego GCC nie używa rejestrów częściowych? i jak dokładnie działają częściowe rejestry w Haswell / Skylake? Pisanie AL wydaje się mieć fałszywą zależność od RAX, a AH jest niespójne
Peter Cordes
Procesory z rodziny P6 albo zatrzymały się na ~ 3 cykle, aby wstawić łączący się uop (Core2 / Nehalem), albo wcześniejsza rodzina P6 (PM, PIII, PII, PPro) po prostu utknęła na (co najmniej?) ~ 6 cykli. Być może jest tak, jak sugerowałeś w 2, czekając, aż pełna wartość reg będzie dostępna poprzez zapis zwrotny do stałego / architektonicznego pliku rejestru.
Peter Cordes
@PeterCordes oh, wiedziałem o łączeniu uopsów przynajmniej dla częściowych straganów z flagami. To ma sens, ale na chwilę zapomniałem, jak to działa; kliknęło raz, ale zapomniałem zrobić notatki
Lewis Kelsey
@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXNie mogę znaleźć przykładu „scalania uop”, który zostałby użyty do rozwiązania tego problemu, tak samo jak w przypadku częściowego stoiska z flagami
Lewis Kelsey
Racja, wczesny P6 zatrzymuje się do czasu zapisu zwrotnego. Core2 i Nehalem wstawiają scalający uop po / przed? tylko opóźnia front-end na krótszy czas. Sandybridge wstawia łączenie uops bez przeciągania. (Ale łączenie AH musi występować w cyklu samoistnym, podczas gdy łączenie AL może być częścią pełnej grupy.) Haswell / SKL w ogóle nie zmienia nazwy AL oddzielnie od RAX, więc mov al, [mem]jest to ładunek mikro-fused + ALU- merge, tylko zmiana nazwy AH, a uop łączący AH nadal występuje samodzielnie. Mechanizmy scalania częściowych flag w tych procesorach są różne, np. Core2 / Nehalem nadal po prostu blokują dla częściowych flag, w przeciwieństwie do częściowej reg.
Peter Cordes