Почему XCHG reg, reg 3 инструкции по микрооперации на современных архитектурах Intel?

Я делаю микро-оптимизацию в критичной для производительности части кода и нахожусь в последовательности инструкций (в синтаксисе AT & T):

add %rax, %rbx
mov %rdx, %rax
mov %rbx, %rdx

Я думал, что у меня наконец был прецедент для xchg, который позволил бы мне побрить инструкцию и написать:

add  %rbx, %rax
xchg %rax, %rdx

Однако, к моему уменьшению, я нашел из таблиц инструкций Agner Fog , что xchg - это 3 команды микрооперации с 2 лайнер цикла на Сэнди-Бридже, Айви-Бридж, Бродвелл, Хасуэлл и даже Скайлак. 3 целых микрооперации и 2 цикла латентности! 3 микрооперации отбрасывают мою 4-1-1-1 каденцию, а задержка в 2 цикла делает ее хуже, чем оригинал, в лучшем случае, так как последние две команды в оригинале могут выполняться параллельно.

Теперь... Я понимаю, что процессор может сломать инструкцию в micro-ops, которые эквивалентны:

mov %rax, %tmp
mov %rdx, %rax
mov %tmp, %rdx 

где tmp является анонимным внутренним регистром, и я предполагаю, что последние два микрооператора могут выполняться параллельно, поэтому латентность составляет 2 цикла.

Учитывая, что переименование регистров происходит на этих микро-архитектурах, для меня не имеет смысла, что это делается таким образом. Почему бы переименовать регистратор не просто заменить метки? Теоретически это будет иметь латентность всего в 1 цикл (возможно, 0?) И может быть представлена ​​в виде одного микрооператора, поэтому было бы намного дешевле.

Ответ 1

Поддержка эффективного xchg нетривиальна и, по-видимому, не стоит дополнительной сложности, требуемой в разных частях ЦП. Реальная микроархитектура процессора намного сложнее, чем ментальная модель, которую вы можете использовать при оптимизации программного обеспечения для нее. Например, спекулятивное выполнение делает все более сложным, поскольку он должен иметь возможность откатываться до точки, в которой произошло исключение.

Эффективность fxch была важна для производительности x87, потому что природа стека x87 делает его (или альтернативы, такие как fld st(2)) трудно избежать. Компилятор FP-код (для целей без поддержки SSE) действительно использует fxch значительную сумму. Похоже, что быстрая fxch была сделана, потому что это было важно не потому, что это было легко. Intel Haswell даже отказался от поддержки для одного-uop fxch. Он все еще имеет нулевую задержку, но декодирует до 2 ups на HSW и позже (до 1 из P5 и PPro через IvyBridge).

xchg обычно легко избежать. В большинстве случаев вы можете просто развернуть цикл, чтобы было нормально, что одно и то же значение теперь находится в другом регистре. например Фибоначчи с add rax, rdx/add rdx, rax вместо add rax, rdx/xchg rax, rdx. Компиляторы обычно не используют xchg reg,reg, и обычно рукописный asm тоже не работает. (Эта проблема с цыпленком/яйцом довольно похожа на loop медленную (Почему медленная инструкция цикла не позволяет Intel эффективно ее реализовать?). loop было бы очень полезно для целых циклов adc для Core2/Nehalem, где цикл adc + dec/jnz вызывает партию с ограниченным флагом.)

Так как xchg все еще медленный на предыдущих процессорах, компиляторы не начнут использовать его с -mtune=generic в течение нескольких лет. В отличие от fxch или mov -elimination, изменение дизайна для поддержки быстрого xchg не помогло бы процессору быстрее запускать весь существующий код, и только обеспечит рост производительности по сравнению с текущим проектом в редких случаях, когда это действительно полезная оптимизация глазок.


Целочисленные регистры сложны с помощью файлов с частичным регистром, в отличие от x87

Существует 4 размера операндов xchg, 3 из которых используют один и тот же код операции с префиксами REX или операнда. (xchg r8,r8 - отдельный код операции, поэтому, возможно, проще сделать декодеры декодировать его по-другому от других). Декодеры уже должны распознавать xchg с операндом памяти как особый из-за неявного префикса lock, но это, вероятно, меньше сложности декодера (транзисторный счет + мощность), если reg-reg формирует все декодированные на одинаковое число uops для разных размеров операндов.

Создание некоторых форм r,r, декодированных на один uop, было бы еще более сложным, потому что инструкции с одним-хуом должны обрабатываться "простыми" декодерами, а также сложным декодером. Таким образом, все они должны были бы разобрать xchg и решить, была ли это единая форма uop или multi-uop.


Процессоры AMD и Intel ведут себя примерно так же с точки зрения программистов, но есть много признаков того, что внутренняя реализация значительно отличается. Например, Intel mov-elim работает только некоторое время, ограниченное некоторыми микроархитектурными ресурсами, но процессоры AMD, устранение делает это в 100% случаев (например, Бульдозер для низкой полосы векторных регистров).

См. руководство по оптимизации Intel, Пример 3-25. Последовательность повторного упорядочения для повышения эффективности инструкций MOV с нулевой задержкой, где они обсуждают возможность перезаписывать результат с нулевой задержкой - movzx, чтобы скорее высвободить внутренний ресурс. (Я попробовал примеры на Haswell и Skylake, и обнаружил, что удаление mov действительно на самом деле работает значительно больше времени, когда вы это делаете, но что это было фактически немного медленнее в общих циклах, а не быстрее. преимущество IvyBridge, которое, вероятно, является узким местом на его 3 портах ALU, но HSW/SKL - только узкое место в конфликтах ресурсов в цепочках депо и, похоже, не беспокоит необходимость использования порта ALU для большего количества инструкций movzx.)

Я точно не знаю, что нужно отслеживать в таблице ограниченного размера (?) для mov-elim. Вероятно, это связано с необходимостью бесплатного ввода регистрационных файлов, когда они больше не нужны, потому что Физический регистр Ограничения на размер файла, а не размер ROB быть узким местом для размера окна вне порядка, Переключение между индексами может сделать это сложнее.

xor -zeroing устраняется в 100% случаев на семействе Intel Sandybridge; он предположил, что это работает путем переименования в физический нулевой регистр, и этот регистр никогда не нужно освобождать.

Если xchg использовал тот же механизм, что и mov-elimination, он также может работать только некоторое время. Он должен будет декодироваться до достаточного количества операций для работы в тех случаях, когда он не обрабатывается при переименовании. (Или, если на этапе выпуска/переименования придется вставлять дополнительные удары, когда xchg будет занимать более 1 мкп, например, когда не ламинирует микро-fused uops с индексированной адресацией режимы, которые не могут оставаться микро-сплавленными в ROB, или при вставке слияния uops для флагов или высокоуровневых частичных регистров. Но это существенное осложнение, которое было бы целесообразно делать, если xchg было общим и важная инструкция.)

Обратите внимание, что xchg r32,r32 должен иметь нулевое расширение до 64 бит,, поэтому он не может быть простой заменой записей RAT (Register Alias ​​Table). Это будет больше похоже на усечение обоих регистров на месте. И обратите внимание, что процессоры Intel никогда не устраняют mov same,same. Он уже должен поддерживать mov r32,r32 и movzx r32, r8 без порт выполнения, поэтому, по-видимому, он имеет некоторые биты, указывающие на то, что rax = al или что-то еще. (И да, Intel HSW/SKL делает это, а не только Айвибридж, несмотря на то, что говорит гид гиганта Agner.)

Мы знаем, что P6 и SnB имеют биты с верхним обнулением, подобные этому, потому что xor eax,eax до setz al избегает блокировки с частичным регистром при чтении eax. HSW/SKL никогда не переименовывают al отдельно, в первую очередь, только ah. Совсем не случайно, что переименование частичных регистров (кроме AH), похоже, было сброшено в том же uarch, который ввел исключение mov (Ivybridge). Тем не менее установка этого бита для двух регистров одновременно будет особым случаем, требующим специальной поддержки.

xchg r64,r64 возможно, просто заменит записи RAT, но декодирование, отличное от случая r32, является еще одним осложнением. Возможно, по-прежнему необходимо инициировать слияние частичных регистров для обоих входов, но add r64,r64 тоже нужно сделать.

Также обратите внимание, что Intel uop (кроме fxch) только когда-либо создает один результат регистрации (плюс флаги). Не касание флагов не "освобождает" выходной слот; Например, mulx r64,r64,r64 по-прежнему принимает 2 uops для создания 2 целых выходов на HSW/SKL, хотя вся "работа" выполняется в умножаемом модуле на порте 1, так же как и в mul r64, который приводит к результату флага).

Даже если это так же просто, как "обменять записи RAT", построение RAT, поддерживающего запись более чем одной записи на компьютер, является сложностью. Что делать при переименовании 4 xchg uops в одной группе проблем? Мне кажется, что логика значительно усложнит ситуацию. Помните, что это должно быть построено из логических ворот/транзисторов. Даже если вы скажете "обрабатывать этот частный случай с помощью ловушки для микрокода", вам нужно построить весь конвейер, чтобы поддержать возможность того, что эта сценария конвейера может принимать такое исключение.

Single-uop fxch требует поддержки для замены RAT-записей (или какого-либо другого механизма) в FP RAT (fRAT), но это отдельный блок аппаратных средств из целочисленного RAT (iRAT). Оставляя это осложнение в iRAT, кажется разумным, даже если вы его используете в fRAT (pre-Haswell).

Проблема/проблема переименования определенно является проблемой для энергопотребления. Обратите внимание, что Skylake расширил множество интерфейсов (устаревшее декодирование и извлечение кэша uop) и вышел на пенсию, но сохранил ограничение ширины и переименования в 4 раза. SKL также добавила реплицированные исполнительные блоки на большее количество портов в фоновом режиме, поэтому пропускная способность канала является узким местом еще больше времени, особенно в коде с сочетанием нагрузок, хранилищ и ALU.

RAT (или файл с целым регистром, IDK) может даже иметь ограниченные порты чтения, поскольку, как представляется, есть некоторые узкие места переднего плана при выпуске/переименовании многих 3-входных uops, таких как add rax, [rcx+rdx]. Я разместил несколько микрофункции (это и последующее сообщение), показывая, что Skylake быстрее, чем Haswell, когда читает множество регистров, например. с микро-слиянием индексированных режимов адресации. Или, может быть, узким местом действительно был какой-то другой микроархитектурный предел.


Но как работает 1-uop fxch?IDK, как это делается в Sandybridge/Ivybridge. В процессорах семейства P6 для поддержки fxch существует дополнительная таблица переназначения. Это может потребоваться только потому, что P6 использует файл реестра для выхода на пенсию с 1 записью на "логический" регистр, а не файл физического регистра (PRF). Как вы говорите, вы ожидаете, что это будет проще, когда даже "холодные" регистровые значения являются лишь указателем на запись PRF. (Источник: Патент США 5 499 352: таблица псевдонимов регистров с плавающей запятой FXCH и матрица регистров с плавающей точкой для выхода на пенсию (описывает Intel P6 uarch).

Одна из основных причин, по которой массив 802 rfRAT включен в настоящее изобретение, логика fRAT является прямым результатом того, как настоящее изобретение реализует инструкцию FXCH.

(Спасибо Andy Glew (@krazyglew), я не думал о в поиске патенты, чтобы узнать о внутренностях ЦП.) Это довольно тяжело, но может дать некоторое представление о бухгалтерии, необходимой для спекулятивного исполнения.

Интересный лакомый кусочек: патент также описывает целое число и упоминает, что существуют некоторые "скрытые" логические регистры, которые зарезервированы для использования с помощью микрокода. (Intel 3-uop xchg почти наверняка использует один из них как временный.)


Мы могли бы получить представление о том, что делает AMD.

Интересно, что AMD имеет 2-юп xchg r,r в K10, Bulldozer-family, Bobcat/Jaguar и Ryzen. (Но Jaguar xchg r8,r8 - 3 раза. Возможно, для поддержки квадратного футляра xchg ah,al без специального uop для замены низкого 16 одного регистра).

Предположительно оба устройства считывают старые значения входных архитектурных регистров до того, как первый обновит RAT. IDK точно, как это работает, поскольку они не обязательно выдаются/переименовываются в одном и том же цикле (но они по крайней мере смежны в потоке uop, поэтому в худшем случае второй uop является первым uop в следующем цикле). Я не знаю, работает ли Haswell 2-uop fxch, или если они делают что-то еще.

Ryzen - это новая архитектура, разработанная после того, как mov-elim была "изобретена", поэтому, по-видимому, они используют ее там, где это возможно. (Bulldozer-family переименовывает векторные перемещения (но только для полосы с низким 128b векторов YMM), Ryzen - первая архитектура AMD, которая делает это для GP-регистров тоже.) xchg r32,r32 и r64,r64 являются нулевой латентностью (переименованы) но все равно 2 uops каждый. (r8 и r16 требуется блок исполнения, потому что они сливаются со старым значением вместо нулевого расширения или копирования всей рег, но все еще остаются только 2 раза).

Ryzen fxch - 1 uop. AMD (например, Intel), вероятно, не тратит много транзисторов на быстрый x87 (например, fmul составляет только 1 за такт и на том же порту, что и fadd), поэтому, по-видимому, они смогли сделать это без особого дополнительной поддержки. Их микрокодированные инструкции x87 (например, fyl2x) быстрее, чем на недавних процессорах Intel, поэтому, возможно, Intel позаботится еще меньше (по крайней мере, о микрокодированной инструкции x87).

Возможно, AMD могла бы сделать xchg r64,r64 один uop тоже, проще, чем Intel. Возможно, даже xchg r32,r32 может быть одиночным uop, так как Intel должен поддерживать mov r32,r32 нулевое расширение без порт выполнения, поэтому, возможно, он может просто установить любой бит с "верхним 32 обнуленным" для поддержки этого. Ryzen не отменяет movzx r32, r8 при переименовании, поэтому, предположительно, существует только бит с верхним 32-нолем, а не бит для другой ширины.


Что Intel могла бы сделать дешево, если бы захотели:

Возможно, что Intel может поддерживать 2-юп xchg r,r способ, которым делает Ryzen (нулевая латентность для форм r32,r32 и r64,r64, или 1c для форм r8,r8 и r16,r16) без лишних дополнительных сложность в критических частях ядра, например этапы выпуска/переименования и выхода на пенсию, которые управляют таблицей псевдонимов регистра (RAT). Но, возможно, нет, если у них не может быть 2 uops, прочитанных "старым" значением регистра, когда первый uop записывает его.

Stuff like xchg ah,al определенно является дополнительным осложнением, поскольку Процессоры Intel больше не переименовывают частичные регистры отдельно, за исключением AH/BH/CH/DH.


xchg задержка на практике на текущем оборудовании

Ваша догадка о том, как она может работать внутри, хороша. Он почти наверняка использует один из внутренних временных регистров (доступен только для микрокода). Однако ваше предположение о том, как они могут переупорядочиваться, слишком ограничено.  На самом деле, одно направление имеет 2c латентность, а другое направление имеет задержку ~ 1 c.

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). uops выпущенные/выполненные счетчики равны, поэтому никакого исключения не произошло. Похоже, что направление dst <- src является медленным, поскольку установка add uops в этой цепочке зависимостей делает вещи медленнее, чем когда они находятся в цепочке зависимостей 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 раза для одной инструкции по-прежнему требуется микрокод ROM. Skylake также увеличил кеш-память uop и часто может быть узким местом на 4-х уровнях скомпилированных доменов в расчете на пропускную способность пропускной способности/переименования часов, если фоновые (или ветхие промахи) не являются узким местом в первую очередь.

Я буквально искал ~ 1% ударов по скорости, поэтому оптимизация рук была разработана в основном коде цикла. К сожалению, это ~ 18 КБ кода, поэтому я даже не пытаюсь рассматривать кеш uop больше.

Это кажется безумным, если только вы в основном не ограничиваете себя оптимизацией уровня в более коротких циклах внутри вашего основного цикла. Любые внутренние петли в основном цикле все еще будут выполняться из кэша uop, и это, вероятно, должно быть там, где вы тратите большую часть своего времени на оптимизацию. Компиляторы обычно выполняют достаточно хорошую работу, чтобы человек не мог делать многое в больших масштабах. Попытайтесь написать свой C или С++ таким образом, чтобы компилятор мог неплохо справиться с этим, но, глядя на крошечные оптимизы в виде глазок, такие как более 18 КБ кода, похоже, спускаются вниз по кроличьей дыре.

Используйте счетчики perf, такие как idq.dsb_uops vs. uops_issued.any, чтобы узнать, сколько из ваших общих uops произошло из кеша uop (DSB = Decode Stream Buffer или что-то еще). Руководство по оптимизации Intel содержит некоторые предложения для других счетчиков perf, чтобы посмотреть на код, который не подходит для кеша uop, например DSB2MITE_SWITCHES.PENALTY_CYCLES, (MITE - путь устаревшего декодирования). Найдите в pdf для DSB, чтобы найти несколько мест, о которых он упоминал.

Счетчики Perf помогут вам найти места с потенциальными проблемами, например. регионы с более высоким, чем средние uops_issued.stall_cycles, могут извлечь выгоду из поиска способов выявления большего количества ILP, если они есть, или от решения проблемы переднего плана, или из-за уменьшения ошибок в ветки.


Как обсуждалось в комментариях, один uop производит не более 1 результата регистрации

Как в стороне, с mul %rbx, вы действительно получаете %rdx и %rax все сразу или ROB технически имеет доступ к нижняя часть результата на один цикл раньше, чем верхняя часть? Или это похоже на то, что "mul" uop переходит в блок умножения, а затем блок умножения выдает два uops прямо в ROB для записи результата в конце?

Терминология: результат умножения не входит в ROB. Он пересылает сеть переадресации на все другие UOP файлы и переходит в PRF.

Команда mul %rbx декодирует до 2 uops в декодерах. Их даже не нужно выпускать в одном цикле, не говоря уже о выполнении в одном цикле.

Однако таблицы инструкций Agner Fogукажите только один номер задержки. Оказывается, что 3 цикла - это латентность от обоих входов до RAX. Минимальная латентность для RDX равна 4c, в соответствии с тестированием InstlatX64 на Haswell и Skylake-X.

Из этого я делаю вывод, что второй uop зависит от первого и существует, чтобы записать высокую половину результата в архитектурный регистр. Port1 uop дает полный результат умножения 128b.

Я не знаю, где результат с высокой половиной жизни до тех пор, пока p6 uop не прочитает его. Возможно, есть какая-то внутренняя очередь между многократным исполнительным модулем и аппаратным обеспечением, подключенным к порту 6. Планируя p6 uop с зависимостью от результата с низкой половиной, который может организовать для p6 uops несколько команд в полете mul для запуска в правильном порядке. Но вместо того, чтобы фактически использовать этот фиктивный вход с низким уровнем, uop будет принимать высокий результат в результате вывода очереди в исполнительном модуле, который подключен к порту 6, и возвращает это как результат. ( Это чистая работа догадки, но я считаю это правдоподобным как возможную внутреннюю реализацию. См. комментарии для некоторых более ранних идей).

Интересно, что согласно таблицы инструкций Agner Fog, на Haswell два uops для mul r64 идут к портам 1 и 6. mul r32 3 uops, и выполняется на 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. Если это точная и не опечатка (что является возможностью), она в значительной степени исключает возможность того, что дополнительный uop является коэффициентом верхней половины.