Почему инструкции x86-64 для 32-битных регистров обнуляют верхнюю часть полного 64-битного регистра?


119

В x86-64 Tour of Intel Manuals я прочитал

Возможно, самым удивительным фактом является то, что такая инструкция, как MOV EAX, EBXавтоматически обнуляет старшие 32 бита RAXрегистра.

В документации Intel (3.4.1.1 Регистры общего назначения в 64-битном режиме в ручной базовой архитектуре), цитируемой в том же источнике, говорится:

  • 64-битные операнды генерируют 64-битный результат в регистре общего назначения назначения.
  • 32-битные операнды генерируют 32-битный результат с расширением нулями до 64-битного результата в целевом регистре общего назначения.
  • 8-битные и 16-битные операнды генерируют 8-битный или 16-битный результат. Старшие 56 бит или 48 бит (соответственно) регистра назначения общего назначения не изменяются в ходе операции. Если результат 8-битной или 16-битной операции предназначен для вычисления 64-битного адреса, явно расширьте регистр знаками до полных 64 бит.

В сборках x86-32 и x86-64 16-битные инструкции, такие как

mov ax, bx

не показывайте такого "странного" поведения, когда верхнее слово eax обнуляется.

Таким образом: какова причина, по которой было введено это поведение? На первый взгляд это кажется нелогичным (но, возможно, причина в том, что я привык к особенностям сборки x86-32).


16
Если вы введете в Google запрос «Частичная задержка в регистре», вы найдете довольно много информации о проблеме, которую они (почти наверняка) пытались избежать.
Джерри Коффин,


4
Не просто «большинство». НАСКОЛЬКО МНЕ ИЗВЕСТНО, все инструкции с r32операндом назначения обнуляют высокий 32, а не объединяются. Например, некоторые ассемблеры заменят его pmovmskb r64, xmmна pmovmskb r32, xmm, сохранив REX, потому что 64-битная конечная версия ведет себя идентично. Несмотря на то, что в разделе «Работа» руководства перечислены все 6 комбинаций 32/64-битного источника и 64/128/256-битного источника отдельно, неявное нулевое расширение формы r32 дублирует явное нулевое расширение формы r64. Мне любопытна реализация HW ...
Питер Кордес,

2
@HansPassant, начинается круговая ссылка.
kchoi

Ответы:


98

Я не AMD и не говорю за них, но я бы поступил так же. Поскольку обнуление верхней половины не создает зависимости от предыдущего значения, ЦП должен ждать. Механизм переименования регистров , по сути, потерпел бы поражение, если бы это не было сделано таким образом.

Таким образом, вы можете писать быстрый код, используя 32-битные значения в 64-битном режиме, без необходимости постоянно явно нарушать зависимости. Без этого поведения каждой 32-битной инструкции в 64-битном режиме пришлось бы ждать чего-то, что происходило раньше, даже если эта высокая часть почти никогда не будет использоваться. (Создание int64-битной версии приведет к потере места в кэше и пропускной способности памяти; x86-64 наиболее эффективно поддерживает 32- и 64-битные размеры операндов )

Странное поведение для 8- и 16-битных операндов. Безумие зависимости - одна из причин, по которой теперь избегают 16-битных инструкций. x86-64 унаследовал это от 8086 для 8-битных и 386 для 16-битных, и решил, что 8- и 16-битные регистры работают в 64-битном режиме так же, как и в 32-битном режиме.


См. Также Почему GCC не использует частичные регистры? для практических деталей того, как записи в 8- и 16-битные частичные регистры (и последующие чтения из полного регистра) обрабатываются реальными процессорами.


8
Я не думаю, что это странно, я думаю, что они не хотели слишком сильно ломаться и сохранили прежнее поведение.
Алексей Фрунзе

5
@Alex, когда они представили 32-битный режим, не было старого поведения для высокой части. Раньше не было высокой части ... Конечно, после этого ее уже нельзя было изменить.
Harold

1
Я говорил о 16-битных операндах, почему в этом случае не обнуляются старшие биты. Их нет в не 64-битных режимах. И это тоже в 64-битном режиме.
Алексей Фрунзе

3
Я интерпретировал ваше «поведение для 16-битных инструкций странно» как «странно, что нулевое расширение не происходит с 16-битными операндами в 64-битном режиме». Отсюда и мои комментарии о том, чтобы оставить его таким же в 64-битном режиме для лучшей совместимости.
Алексей Фрунзе

8
@ Алекс, понятно. Хорошо. Я не думаю, что это странно с этой точки зрения. Просто с точки зрения «оглядываясь назад, может быть, это была не такая уж хорошая идея». Думаю, я должен был быть яснее :)
Harold

9

Это просто экономит место в инструкциях и наборе инструкций. Вы можете сразу же переместить небольшие значения в 64-битный регистр, используя существующие (32-битные) инструкции.

Это также избавляет вас от необходимости кодировать 8-байтовые значения MOV RAX, 42, когдаMOV EAX, 42 их можно использовать повторно.

Эта оптимизация не так важна для 8- и 16-битных операций (потому что они меньше), и изменение правил там также нарушит старый код.


7
Если это правильно, не было бы больше смысла в расширении знака, а не в расширении 0?
Damien_The_Unbeliever

16
Расширение знака происходит медленнее, даже на оборудовании. Нулевое расширение может выполняться параллельно с любыми вычислениями, производящими нижнюю половину, но знаковое расширение не может быть выполнено до тех пор, пока (по крайней мере, знак) не будет вычислена нижняя половина.
Джерри Коффин,

13
Другой связанный с этим трюк - использовать, XOR EAX, EAXпотому XOR RAX, RAXчто потребуется префикс REX.
Нил

3
@Nubok: Конечно, они могли добавить кодировку movzx / movsx, которая принимает немедленный аргумент. Большая часть времени это более удобно иметь верхние биты обнуляются, так что вы можете использовать значение в качестве индекса массива (потому что все регистры должны быть одинаковым размером в эффективном адресе: [rsi + edx]не допускается). Конечно, еще одна важная причина - избегать ложных зависимостей / частичных остановок регистрации (другой ответ).
Питер Кордес

4
и изменение правил там также нарушит старый код. Старый код в любом случае не может работать в 64-битном режиме (например, 1-байтовый inc / dec - это префиксы REX); это не имеет значения. Причина, по которой не устраняются недостатки x86, заключается в меньшем количестве различий между длинным режимом и режимами совместимости / устаревания, поэтому меньше инструкций приходится декодировать по-разному в зависимости от режима. AMD не знала, что AMD64 завоюет популярность, и, к сожалению, была очень консервативной, поэтому для поддержки потребовалось меньше транзисторов. В долгосрочной перспективе было бы хорошо, если бы компиляторы и люди запомнили, какие вещи работают по-разному в 64-битном режиме.
Питер Кордес

1

Без расширения нуля до 64 битов это будет означать, что инструкция, из raxкоторой выполняется чтение, будет иметь 2 зависимости для своего raxоперанда (инструкция, которая выполняет запись, eaxи инструкция, которая записывает raxдо нее), это означает, что 1) ROB должен иметь записи для множественные зависимости для одного операнда, что означает, что ROB потребует больше логики и транзисторов и займет больше места, а выполнение будет медленнее в ожидании ненужной второй зависимости, выполнение которой может занять много времени; или, как вариант 2), что, как я предполагаю, происходит с 16-битными инструкциями, этап распределения, вероятно, останавливается (т.е. если RAT имеет активное выделение для axзаписи и eaxпоявляется чтение, он останавливается до тех пор, пока axзапись не прекратится).

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

Единственное преимущество ненулевого расширения - это обеспечение включения битов более высокого порядка rax, например, если он изначально содержит 0xffffffffffffffff, результатом будет 0xffffffff00000007, но у ISA очень мало причин давать эту гарантию такой ценой, и более вероятно, что преимущества нулевого расширения на самом деле потребуются больше, поэтому это экономит лишнюю строку кода mov rax, 0. Гарантируя она всегда будет равна нулю продлен до 64 бит, компиляторы могут работать с этой аксиомой в виду , в то время как в mov rdx, rax, raxтолько должен ждать своей единственной зависимости, то есть он может начать выполнение быстрее и удалиться, освобождая исполнительных блоков. Кроме того, он также позволяет использовать более эффективные нулевые идиомы, такие как xor eax, eaxноль, raxбез использования байта REX.


Частичные флаги на Skylake, по крайней мере, работают, имея отдельные входы для CF по сравнению с любым из SPAZO. (Так cmovbeже 2 мопса, но cmovb1). Но ни один процессор, который выполняет частичное переименование регистров, не делает это так, как вы предлагаете. Вместо этого они вставляют UOP слияния, если частичный регистр переименован отдельно от полного (т.е. "грязный"). См. Почему GCC не использует частичные регистры? и как именно работают частичные регистры на Haswell / Skylake? Написание AL, похоже, ложно зависит от RAX, а AH непоследовательно
Питер Кордес

Процессоры семейства P6 либо остановились на ~ 3 цикла, чтобы вставить объединяющий uop (Core2 / Nehalem), либо более ранние модели семейства P6 (PM, PIII, PII, PPro) просто остановились на (как минимум?) ~ 6 циклов. Возможно, это похоже на то, что вы предложили в 2, ожидая, пока полное значение reg будет доступно через обратную запись в файл постоянного / архитектурного реестра.
Питер Кордес,

@PeterCordes, о, я знал о слиянии мопов по крайней мере для частичных стендов с флагами. Имеет смысл, но я на минуту забыл, как это работает; он щелкнул один раз, но я забыл сделать заметки
Льюис Келси

@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 EAXЯ не могу найти пример «слияния uop», который можно было бы использовать для решения этой проблемы, то же самое для частичного сваливания флага
Льюис Келси,

Правильно, ранний P6 просто зависает до обратной записи. Core2 и Nehalem вставляют объединяющий uop после / до? только задержка фронтенда на более короткое время. Вставки Sandybridge сливаются без остановки. (Но слияние AH должно происходить в цикле само по себе, в то время как слияние AL может быть частью полной группы.) Haswell / SKL вообще не переименовывает AL отдельно от RAX, так mov al, [mem]что это микропредохранительная нагрузка + ALU- merge, переименовывая только AH, а UOP-объединение AH все еще выдает один. Механизмы слияния частичных флагов в этих процессорах различаются, например, Core2 / Nehalem по-прежнему просто останавливается для частичных флагов, в отличие от частичной регистрации.
Питер Кордес,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.