Почему XCHG reg, reg 3 микрооперационная инструкция по современным архитектурам Intel?
Я занимаюсь микрооптимизацией критически важной части своего кода и натолкнулся на последовательность инструкций (в синтаксисе AT&T):
add %rax, %rbx
mov %rdx, %rax
mov %rbx, %rdx
Я думал, что у меня наконец-то был случай xchg
что позволило бы мне побрить инструкцию и написать:
add %rbx, %rax
xchg %rax, %rdx
Однако, к моему дневнику, я обнаружил из таблиц инструкций Агнера Фога, что xchg
3-х микрооперационная инструкция с задержкой в 2 цикла на Сэнди Бридж, Айви Бридж, Бродвелл, Хасвелл и даже Скайлэйк. 3 целых микрооперации и 2 цикла задержки! 3 микрооперации сбрасывают мою частоту 4-1-1-1, а задержка в 2 цикла делает его хуже, чем оригинал, в лучшем случае, поскольку последние 2 инструкции в оригинале могут выполняться параллельно.
Теперь... Я понимаю, что процессор может разбивать инструкцию на микрооперации, которые эквивалентны:
mov %rax, %tmp
mov %rdx, %rax
mov %tmp, %rdx
где tmp
является анонимным внутренним регистром, и я предполагаю, что последние два микрооперации могут выполняться параллельно, поэтому задержка составляет 2 цикла.
Учитывая, что переименование регистров происходит на этих микроархитектурах, для меня не имеет смысла, что это делается таким образом. Почему бы не переименовать регистр renamer? Теоретически, это будет иметь задержку всего в 1 цикл (возможно, 0?) И может быть представлено как одна микрооперация, так что это будет намного дешевле.
1 ответ
Поддержка эффективной xchg
является нетривиальным и, по-видимому, не стоит дополнительной сложности, которая потребовалась бы для различных частей ЦП. Микроархитектура реального процессора намного сложнее, чем интеллектуальная модель, которую вы можете использовать при оптимизации программного обеспечения для нее. Например, умозрительное выполнение все усложняет, потому что оно должно иметь возможность откатиться до точки, где произошло исключение.
Изготовление fxch
эффективность была важна для производительности x87, потому что природа стека x87 делает это (или альтернативы как fld st(2)
Трудно избежать. Генерируемый компилятором код FP (для целей без поддержки SSE) действительно использует fxch
значительное количество. Кажется, что быстро fxch
было сделано, потому что это было важно, а не потому, что это легко. Intel Haswell даже отказался от поддержки Single-UOP fxch
, Это все еще нулевая задержка, но декодируется до 2 моп в HSW и позже (по сравнению с 1 в P5 и PPro через IvyBridge).
xchg
обычно легко избежать. В большинстве случаев вы можете просто развернуть цикл, так что теперь одно и то же значение находится в другом регистре. например, Фибоначчи с add rax, rdx
/ add rdx, rax
вместо add rax, rdx
/ xchg rax, rdx
, Компиляторы обычно не используют xchg reg,reg
и, как правило, рукописный асм тоже нет. (Эта проблема курица / яйцо очень похожа на loop
быть медленным ( почему инструкция цикла слишком медленная? Неужели Intel не реализовала ее эффективно?). loop
было бы очень полезно для adc
петли на Core2/Nehalem, где adc
+ dec/jnz
цикл приводит к частичной остановке флага.)
поскольку xchg
все еще работает медленно на предыдущих процессорах, компиляторы не начали бы использовать его с -mtune=generic
в течение нескольких лет. В отличие от fxch
или же mov
устранение, изменение дизайна для поддержки быстро xchg
не помогло бы ЦП быстрее выполнить большую часть существующего кода, а лишь обеспечило бы прирост производительности по сравнению с текущим проектом в тех редких случаях, когда это на самом деле полезная оптимизация глазка.
Целочисленные регистры усложняются частичным регистром, в отличие от x87
Есть 4 размера операндов xchg
, 3 из которых используют один и тот же код операции с префиксами REX или размером с операнд. (xchg r8,r8
это отдельный код операции, поэтому, вероятно, проще сделать так, чтобы декодеры декодировали его не так, как другие). Декодеры уже должны распознавать xchg
с операндом памяти как особенным, из-за неявного lock
префикс, но это, вероятно, меньше сложность декодера (количество транзисторов + мощность), если reg-reg формирует все декодирование с одинаковым числом мопов для разных размеров операндов.
Делая некоторые r,r
Декодирование форм на один моп будет еще более сложным, потому что инструкции с одним мопом должны обрабатываться как "простыми" декодерами, так и сложным декодером. Таким образом, они все должны быть в состоянии разобрать xchg
и решить, была ли это единичная или многопользовательская форма.
Процессоры AMD и Intel ведут себя примерно одинаково с точки зрения программиста, но есть много признаков того, что внутренняя реализация сильно отличается. Например, Intel mov-elission работает только иногда, ограниченный какими-то микроархитектурными ресурсами, но процессоры AMD, которые выполняют mov-elission, делают это 100% времени (например, Bulldozer для нижней полосы векторных регистров).
См. Руководство по оптимизации Intel, пример 3-25. Последовательность переупорядочения для повышения эффективности инструкций MOV с нулевой задержкой, где они обсуждают перезапись нулевой задержки. movzx
результат сразу, чтобы быстрее освободить внутренний ресурс. (Я попробовал примеры на Haswell и Skylake, и обнаружил, что удаление-mov на самом деле работало значительно больше времени при этом, но на самом деле оно было немного медленнее в полных циклах, чем быстрее. Пример должен был показать Преимущество IvyBridge, которое, вероятно, является узким местом на его 3 портах ALU, но HSW/SKL только узкое место на конфликтах ресурсов в цепочках dep и, похоже, не беспокоит необходимость использования порта ALU для большей части movzx
инструкции.)
Я не знаю точно, что нужно отслеживать в таблице ограниченного размера (?) Для удаления mov. Вероятно, это связано с необходимостью как можно скорее освободить записи в файле реестра, когда они больше не нужны, поскольку ограничения размера физического файла реестра, а не размера ROB, могут стать узким местом для размера окна, вышедшего из строя. Смена индексов может сделать это сложнее.
xor
- обнуление исключается в 100% случаев на семействе Intel Sandybridge; предполагается, что это работает путем переименования в регистр физического нуля, и этот регистр никогда не нужно освобождать.
Если xchg
использовал тот же механизм, что и mov-elission, он также, вероятно, мог работать только некоторое время. Он должен был бы декодировать до достаточного числа мопов, чтобы работать в тех случаях, когда он не обрабатывается при переименовании. (Или же этап выпуска / переименования должен был бы вставить дополнительные мопы, когда xchg
будет занимать более 1 моп, как это происходит, если не ламинировать микросинверсионные мопы с индексированными режимами адресации, которые не могут оставаться микросинхронизированными в ROB, или при вставке объединяющихся мопов для флагов или регистров частичного числа с высоким значением 8. Но это существенное осложнение, которое стоило бы делать, только если xchg
была общая и важная инструкция.)
Обратите внимание, что xchg r32,r32
должен обнулить оба результата до 64 битов, поэтому это не может быть простым обменом записей RAT (Register Alias Table). Это было бы больше похоже на усечение обоих регистров на месте. И обратите внимание, что процессоры Intel никогда не устраняют mov same,same
, Это уже нужно поддерживать mov r32,r32
а также movzx r32, r8
без порта выполнения, так что, предположительно, он имеет некоторые биты, которые указывают rax = al
или что-то. (И да, Intel HSW/SKL делает это, а не только Ivybridge, несмотря на то, что говорится в руководстве по микроархам Агнера.)
Мы знаем, что P6 и SnB имели биты с верхним нулем, как это, потому что xor eax,eax
до setz al
избегает частичного останова при чтении eax. HSW/SKL никогда не переименовывать al
отдельно на первом месте, только ah
, Это не может быть совпадением, что переименование частичного регистра (кроме AH), по-видимому, было отброшено в том же самом Uarch, который ввел mov-elission (Ivybridge). Тем не менее, установка этого бита для 2-х регистров одновременно будет особым случаем, требующим специальной поддержки.
xchg r64,r64
может быть, просто поменять записи RAT, но декодирование, отличное от случая r32, является еще одним осложнением. Может потребоваться частичное объединение регистров для обоих входов, но add r64,r64
нужно сделать это тоже.
Также обратите внимание, что Intel UOP (кроме fxch
) только когда-либо дает один результат регистра (плюс флаги). Не трогать флаги не освобождает выходной слот; Например mulx r64,r64,r64
по-прежнему требуется 2 мопа для получения 2 целочисленных выходов в HSW/SKL, хотя вся "работа" выполняется в модуле умножения на порту 1, так же, как mul r64
который дает результат флага.)
Даже если это так же просто, как "поменять записи RAT", создание RAT, которое поддерживает запись более одной записи за моп, является сложностью. Что делать при переименовании 4 xchg
моп в одной группе вопросов? Мне кажется, что это значительно усложнит логику. Помните, что это должно быть построено из логических вентилей / транзисторов. Даже если вы скажете "обрабатывать этот особый случай с ловушкой для микрокода", вам придется построить весь конвейер, чтобы обеспечить возможность того, что стадия конвейера может принять такого рода исключение.
Single-моп fxch
требуется поддержка для замены записей RAT (или другого механизма) в FP RAT (f RAT), но это отдельный блок аппаратного обеспечения от целочисленного RAT (iRAT). Исключение этого осложнения в iRAT кажется разумным, даже если оно есть в f RAT (до Haswell).
Однако сложность проблемы с переименованием является проблемой энергопотребления. Обратите внимание, что Skylake значительно расширил внешний интерфейс (унаследованное декодирование и выборка из кэша UOP) и удаление, но сохранил ограничение выпуска / переименования в 4 раза. SKL также добавил реплицированные исполнительные блоки на большее количество портов в бэкэнде, поэтому пропускная способность проблемы является узким местом даже чаще, особенно в коде со смесью нагрузок, хранилищ и ALU.
RAT (или целочисленный регистровый файл, IDK) может даже иметь ограниченные порты чтения, так как, похоже, существуют некоторые узкие места в интерфейсе при выпуске / переименовании многих мопов с 3 входами add rax, [rcx+rdx]
, Я опубликовал несколько микробенчмарков ( этот и следующий пост), показывающих, что Skylake быстрее, чем Haswell, при чтении большого количества регистров, например, с микросинтезом индексированных режимов адресации. Или, возможно, узким местом действительно был какой-то другой микроархитектурный предел.
Но как работает 1-моп fxch
Работа? ИДК, как это делается в Сэндибридже / Айвибридже. В процессорах семейства P6 существует дополнительная таблица переназначения для поддержки FXCH
, Это может понадобиться только потому, что P6 использует файл регистров выхода на пенсию с 1 записью на "логический" регистр вместо файла физических регистров (PRF). Как вы говорите, вы ожидаете, что это будет проще, когда даже "холодные" значения регистров являются просто указателем на запись PRF. (Источник: патент США 5499352: таблица псевдонимов регистров с плавающей запятой FXCH и массив регистров с плавающей запятой списания (описывает Intel P6 uarch).
Одна из основных причин, по которой массив 802 rf RAT включен в настоящее изобретение, логика f RAT является прямым результатом способа, которым настоящее изобретение реализует инструкцию FXCH.
(Спасибо Энди Глеу Krazy Glew, я не думал искать патенты, чтобы узнать о внутренних процессорах процессора.) Это довольно тяжело, но может дать некоторое представление о бухгалтерском учете, необходимом для спекулятивного исполнения.
Интересный тидбит: в патенте также описывается целое число и упоминается, что существуют некоторые "скрытые" логические регистры, которые зарезервированы для использования микрокодом. (Intel 3-моп xchg
почти наверняка использует один из них как временный.)
Мы могли бы получить некоторое представление о том, что делает AMD.
Интересно, что у AMD есть 2-ухоп xchg r,r
в K10, семейство бульдозеров, Bobcat/Jaguar и Ryzen. (Но ягуар xchg r8,r8
3 мопс. Может быть, чтобы поддержать xchg ah,al
угловой корпус без специального мопа для замены младших 16 единичных рег).
Предположительно, оба мопа читают старые значения входных архитектурных регистров, прежде чем первый обновит RAT. IDK точно, как это работает, поскольку они не обязательно выпускаются / переименовываются в одном и том же цикле (но они, по крайней мере, являются непрерывными в потоке UOP, так что в худшем случае 2-й UOP является первым UOP в следующем цикле). Я понятия не имею, если 2-моп Хасвелл fxch
работает аналогично, или если они делают что-то еще.
Ryzen - это новая архитектура, разработанная после того, как mov-elven tion была "изобретена", поэтому, по-видимому, они используют ее везде, где это возможно. (Семейство Bulldozer переименовывает векторные перемещения (но только для низкой 128-полосной полосы векторов YMM); Ryzen - первая архитектура AMD, которая сделала это и для регистров GP.) xchg r32,r32
а также r64,r64
с нулевой задержкой (переименована), но по-прежнему 2 моп каждый. (r8
а также r16
нужна исполнительная единица, потому что они объединяются со старым значением вместо расширения нуля или копирования всего регистра, но все еще имеют только 2 мопа).
Ryzen - х fxch
1 моп. AMD (как и Intel), вероятно, не тратит много транзисторов на быстрое создание x87 (например, fmul
только 1 за такт и на том же порту, что и fadd
), так что, по-видимому, они смогли сделать это без особой поддержки. Их микрокодированные инструкции x87 (например, fyl2x
) быстрее, чем на современных процессорах Intel, поэтому, возможно, Intel заботится еще меньше (по крайней мере, о микрокодированной инструкции x87).
Может быть, AMD могла бы сделать xchg r64,r64
один моп тоже, легче чем Intel. Может быть даже xchg r32,r32
может быть один UOP, так как, как и Intel, он должен поддерживать mov r32,r32
нулевое расширение без порта выполнения, так что, возможно, он мог бы просто установить любой существующий бит "верхний 32 обнуляется" для поддержки этого. Райзен не устраняет movzx r32, r8
при переименовании, так что, предположительно, есть только верхний 32-нулевой бит, а не биты для других значений ширины.
Что Intel могла бы сделать дешево, если бы они хотели:
Вполне возможно, что Intel может поддерживать 2-UOP xchg r,r
как делает Ryzen (нулевая задержка для r32,r32
а также r64,r64
формы, или 1с для r8,r8
а также r16,r16
формы) без особых сложностей в критических частях ядра, таких как этапы выпуска / переименования и вывода из эксплуатации, которые управляют таблицей псевдонимов регистра (RAT). Но, возможно, нет, если они не могут 2 мопа прочитать "старое" значение регистра, когда первый моп записывает его.
Вещи как xchg ah,al
Это определенно дополнительная сложность, поскольку процессоры Intel больше не переименовывают частичные регистры отдельно, кроме AH / BH / CH / DH.
xchg
задержка на практике на текущем оборудовании
Ваше предположение о том, как это может работать внутри, хорошо. Он почти наверняка использует один из внутренних временных регистров (доступных только для микрокода). Ваше предположение о том, как они могут изменить порядок, слишком ограничено. Фактически, одно направление имеет задержку 2с, а другое направление имеет задержку ~1с.
00000000004000e0 <_start.loop>:
4000e0: 48 87 d1 xchg rcx,rdx # slow version
4000e3: 48 83 c1 01 add rcx,0x1
4000e7: 48 83 c1 01 add rcx,0x1
4000eb: 48 87 ca xchg rdx,rcx
4000ee: 48 83 c2 01 add rdx,0x1
4000f2: 48 83 c2 01 add rdx,0x1
4000f6: ff cd dec ebp
4000f8: 7f e6 jg 4000e0 <_start.loop>
Этот цикл выполняется в ~8,06 циклах на итерацию на Skylake. Задний ход xchg
Операнды заставляют его работать в ~6,23c циклов за итерацию (измеряется с perf stat
в Linux). Счетчики выданных / выполненных мопов равны, поэтому исключения не произошло. Похоже, dst <- src
направление медленное, так как add
моп в этой цепочке зависимостей делает вещи медленнее, чем когда они на dst -> src
цепочка зависимостей.
Если вы когда-нибудь захотите использовать xchg reg,reg
на критическом пути (причины размера кода?), сделайте это с dst -> src
направление на критическом пути, потому что это только около 1с задержки.
Другие побочные темы из комментариев и вопроса
3 микрооперации сбрасывают мою 4-1-1-1 каденцию
Декодеры семейства Sandybridge отличаются от Core2/Nehalem. Они могут производить до 4 моп, а не 7, поэтому шаблоны 1-1-1-1
, 2-1-1
, 3-1
, или же 4
,
Также имейте в виду, что если последний uop является тем, который может макрособъединиться, они будут висеть на нем до следующего цикла декодирования, если первая инструкция в следующем блоке является jcc
, (Это выигрыш, когда код запускается несколько раз из кэша UOP каждый раз, когда он декодируется. И все же, как правило, это 3 пропуска на тактовую частоту декодирования.)
Skylake имеет дополнительный "простой" декодер, так что он может сделать 1-1-1-1-1
вплоть до 4-1
Я думаю, но> 4 мопа для одной инструкции все еще требует микрокода ПЗУ. Skylake также усилил кэш UOP и часто может стать узким местом для 4-х циклов слияния с доменом слияния в течение одного такта проблемы / переименования пропускной способности, если серверная часть (или пропущенная ветвь) не является узким местом в первую очередь.
Я буквально ищу ~1% скачков скорости, поэтому ручная оптимизация работала над кодом основного цикла. К сожалению, это ~18 КБ кода, поэтому я даже не пытаюсь больше рассматривать кэш UOP.
Это кажется сумасшедшим, если только вы не ограничиваетесь оптимизацией на уровне asm в более коротких циклах внутри основного цикла. Любые внутренние циклы внутри основного цикла будут по-прежнему выполняться из кэша UOP, и это, вероятно, должно быть именно там, где вы проводите большую часть своего времени в оптимизации. Компиляторы, как правило, выполняют достаточно хорошую работу, поэтому для человека непрактично много делать в больших масштабах. Попытайтесь написать свой C или C++ таким образом, чтобы компилятор, конечно, мог с ним хорошо поработать, но поиск крошечных оптимизаций в виде глазка, таких как код размером более 18 КБ, кажется, будто спускается в кроличью нору.
Используйте счетчики перфорации как idq.dsb_uops
против uops_issued.any
чтобы увидеть, сколько всего ваших мопов поступило из кеша мопов (DSB = Decode Stream Buffer или что-то в этом роде). В руководстве по оптимизации Intel есть несколько советов для других счетчиков перфорации, чтобы найти код, который не помещается в кэш UOP, например: DSB2MITE_SWITCHES.PENALTY_CYCLES
, (MITE - это путь унаследованного декодирования). Поищите в pdf DSB, чтобы найти несколько мест, о которых упоминается.
Счетчики производительности помогут вам найти места с потенциальными проблемами, например, в регионах с уровнем выше среднего uops_issued.stall_cycles
может извлечь выгоду из поиска способов раскрытия большего количества ILP, если таковые имеются, или из решения интерфейсной проблемы, или из-за уменьшения числа ошибочных прогнозов в филиалах.
Как обсуждалось в комментариях, один моп дает максимум 1 результат регистрации
В сторону, с
mul %rbx
ты действительно получаешь%rdx
а также%rax
внезапно или технически у ROB есть доступ к нижней части результата на один цикл раньше, чем к верхней части? Или это похоже на то, что "мул" переходит в модуль умножения, а затем модуль умножения выдает два мопа прямо в ROB, чтобы записать результат в конце?
Терминология: результат умножения не входит в ROB. Он переходит через сеть пересылки к тому, что читают другие мопы, и переходит в PRF.
mul %rbx
инструкция декодирует до 2 моп в декодерах. Они даже не должны выпускать в одном и том же цикле, не говоря уже о выполнении в одном и том же цикле.
Однако в таблицах команд Agner Fog указан только один номер задержки. Оказывается, что 3 цикла - это задержка от обоих входов до RAX. Минимальная задержка для RDX составляет 4 с, согласно тестированию InstlatX64 на Haswell и Skylake-X.
Из этого я делаю вывод, что второй моп зависит от первого и существует для записи верхней половины результата в архитектурный регистр. Port1 uop дает полный результат умножения на 128b.
Я не знаю, где живет результат с половиной, пока p6 uop не прочитает его. Возможно, существует какая-то внутренняя очередь между модулем многократного выполнения и оборудованием, подключенным к порту 6. Путем планирования p6-мопа с зависимостью от результата low-half, это может организовать p6-моп из нескольких в полете. mul
Инструкции для запуска в правильном порядке. Но затем вместо того, чтобы фактически использовать этот фиктивный ввод младшей половины, моп получал бы результат высокой половины из вывода очереди в исполнительном модуле, подключенном к порту 6, и возвращал его в качестве результата. (Это чисто догадка, но я думаю, что это правдоподобно как одна из возможных внутренних реализаций. См. Комментарии для некоторых более ранних идей).
Интересно, что согласно таблицам инструкций Агнера Фога, на Haswell два мопа для mul r64
перейти к портам 1 и 6. mul r32
равен 3 мопам и работает на p1 + p0156. Агнер не говорит, действительно ли это 2p1 + p0156
или же p1 + 2p0156
как он делает для некоторых других insns. (Однако он говорит, что mulx r32,r32,r32
работает на p1 + 2p056
(обратите внимание, что p056 не включает p1).)
Еще более странно, что он говорит, что Skylake работает mulx r64,r64,r64
на p1 p5
но mul r64
на p1 p6
, Если это точно, а не опечатка (что возможно), это в значительной степени исключает возможность того, что дополнительный моп является множителем верхней половины.