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

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

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

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

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

Тем не менее, к моему разуму, который я нашел из таблиц инструкций xchg Fog, xchg – это 3-х микрооперационная команда с 2- xchg задержкой на Sandy Bridge, Ivy Bridge, Broadwell, Haswell и даже Skylake. 3 целых микрооперации и 2 цикла латентности! 3 микрооперации отбрасывают мою 4-1-1-1 каденцию, а задержка в 2 цикла делает ее хуже, чем оригинал, в лучшем случае, так как последние две команды в оригинале могут выполняться параллельно.

Теперь … Я понимаю, что процессор может сломать инструкцию в микрооперациях, которые эквивалентны:

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

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

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

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

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

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

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


Целочисленные регистры сложны в частичном регистре, в отличие от x87

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

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


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

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

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

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

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

Обратите внимание, что xchg r32,r32 имеет нулевое значение – 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 никогда не переименовывают отдельно, в первую очередь, только ah . Совсем не случайно, что переименование частичных регистров (кроме AH), похоже, было сброшено в том же uarch, который ввел исключение mov (Ivybridge). Тем не менее установка этого бита для 2 регистров одновременно будет особым случаем, требующим специальной поддержки.

xchg r64,r64 возможно, просто 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 в одной группе проблем? Мне кажется, что логика значительно усложнит ситуацию. Помните, что это должно быть построено из логических ворот / транзисторов. Даже если вы скажете «обрабатывать этот особый случай с помощью ловушки для микрокода», вам нужно построить весь конвейер, чтобы поддержать возможность того, что эта сценария трубопровода может принимать такое исключение.

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

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

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


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

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

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

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


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

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

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

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

fxch Ryzen – 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 zero-extension без 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 пишет его.

Такие вещи, как 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 addops в эту цепочку зависимостей делает вещи медленнее, чем когда они находятся в цепочке зависимостей dst -> src .

Если вы когда-нибудь захотите использовать xchg reg,reg на критическом пути (причины размера кода?), xchg reg,reg это с помощью направления dst -> src на критическом пути, потому что это всего лишь 1 c латентность.


Другие темы из комментариев и вопрос

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, и это, вероятно, должно быть там, где вы тратите большую часть своего времени на оптимизацию. Составители обычно выполняют достаточно хорошую работу, чтобы человек не мог делать многое в больших масштабах. Попытайтесь написать свой C или C ++ таким образом, чтобы компилятор мог с ним неплохо справиться, но, глядя на крошечные оптимизы в виде глазок, подобные этому более 18 КБ кода, похоже, спускаются по кроличьей дыре.

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

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


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

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

Терминология: результат умножения не входит в ROB. Он пересылает сеть переадресации на любые другие uops, читает ее и переходит в 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 и возвращает это как результат. ( Это чистая работа , но я думаю, что это правдоподобно, как одна возможная внутренняя реализация. См. Комментарии к некоторым более ранним идеям).

Интересно, что, согласно таблицам инструкций Агнера Фога , на mul r64 два выхода для mul r64 переходят в порты 1 и 6. mul r32 составляет 3 mul r32 и работает на 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 является мультипликатором верхней половины.

  • Сделать муравей тихий без флага -q?
  • Эффективное умножение матрицы 4x4 (C vs assembly)
  • Как вы прокручиваете загруженные в настоящее время сборки?
  • Использование разных версий одной и той же сборки в одной папке
  • Как я могу перечислить все загруженные сборки?
  • Инициализировать библиотеку при загрузке сборки
  • Как загрузить сборку во время выполнения перед событием AssemblyResolve?
  • Сколько циклов процессора требуется для каждой инструкции сборки?
  • Как определить, была ли assembly .NET построена для x86 или x64?
  • Visual Studio 2010 не создает перед запуском при изменении кода
  • Проверьте, равен ли регистр нулю с помощью CMP reg, 0 против OR reg, reg?
  • Interesting Posts

    Откройте хром и запустите страницу настроек

    Почему определение classа как окончательного улучшает производительность JVM?

    Оглавление на основе пользовательского стиля заголовка

    Более эффективная программа сжатия файлов для многих идентичных файлов?

    Что такое null в Java?

    Можно ли запускать игры через удаленный рабочий стол?

    Как вы соединяете часть файла avi с кодировкой xvid с помощью ffmpeg? (Никаких проблем с другими файлами)

    можете ли вы разместить частный repository для своей организации для использования с npm?

    Как обрабатывать несколько таймеров обратного отсчета в ListView?

    Когда используется MySQL BLOB?

    JavaFX – может ли он действительно быть развернут в браузере?

    Понимая power lan – с технической точки зрения, почему он становится медленнее

    Как вы разрешаете ошибку «Доступ запрещен: указанный пользователь не является членом групп TelnetClients»??

    Dnsmasq не работает, чтобы указывать локальные адреса на 127.0.0.1

    OWIN’s GetExternalLoginInfoAsync всегда возвращает null

    Давайте будем гением компьютера.