Медленная jmp-инструкция

В ответ на мой вопрос Преимущества использования 32-битных регистров / инструкций в x86-64 я начал измерять стоимость инструкций. Я знаю, что это делалось несколько раз (например, Agner Fog), но я делаю это для развлечения и самообразования.

Мой тестовый код довольно прост (для простоты здесь как псевдокод, на самом деле на ассемблере):

for(outer_loop=0; outer_loop<NO;outer_loop++){
    operation  #first
    operation  #second
    ...
    operation #NI-th
} 

Но все же некоторые вещи следует учитывать.

  1. Если внутренняя часть цикла велика (большой NI>10^7), все содержимое цикла не помещается в кэш инструкций и, следовательно, должно загружаться снова и снова, в результате чего скорость ОЗУ определяет время, необходимое для выполнения. Например, для больших внутренних частей xorl %eax, %eax (2 байта) на 33% быстрее, чем xorq %rax, %rax (3 байта).
  2. Если NI мало и весь цикл легко помещается в кэш инструкций, то xorl %eax, %eax и xorq %rax, %rax одинаково быстры и могут выполняться 4 раза за такт.

Однако эта простая модель не выдерживает критики jmp-инструкции. Для jmp-инструкции мой тестовый код выглядит следующим образом:

for(outer_loop=0; outer_loop<NO;outer_loop++){
    jmp .L0
    .L0: jmp .L1
    L1: jmp L2
    ....
}

И вот результаты:

  1. Для "больших" размеров цикла (уже для NI>10^4) я измеряю 4,2 нс / jmp-инструкцию (это равняется 42 байтам, загруженным из ОЗУ, или примерно 12 тактам на моей машине).
  2. Для небольших размеров цикла (NI<10^3) я измеряю 1 нс / jmp- инструкцию (что составляет около 3 тактовых циклов, что звучит правдоподобно - таблицы Агнера Фога показывают стоимость 2 тактовых циклов).

Инструкция jmp LX использует 2-байтовую eb 00 кодировку.

Итак, мой вопрос: Чем может быть объяснение дороговизны jmp-инструкции в "больших" циклах?

PS: Если вы хотите попробовать его на своем компьютере, вы можете загрузить сценарии с здесь, просто запустите sh jmp_test.sh в папке src.


Изменить: экспериментальные результаты, подтверждающие теорию размера BTB Питера.

В следующей таблице показаны циклы на инструкцию для различных значений ǸI (относительно NI = 1000):

|oprations/ NI        | 1000 |  2000|  3000|  4000|  5000| 10000|
|---------------------|------|------|------|------|------|------|
|jmp                  |  1.0 |  1.0 |  1.0 |  1.2 |  1.9 |   3.8|
|jmp+xor              |  1.0 |  1.2 |  1.3 |  1.6 |  2.8 |   5.3|
|jmp+cmp+je (jump)    |  1.0 |  1.5 |  4.0 |  4.4 |  5.5 |   5.5|
|jmp+cmp+je (no jump) |  1.0 |  1.2 |  1.3 |  1.5 |  3.8 |   7.6|

Это можно увидеть:

  1. Для инструкции jmp (пока неизвестный) ресурс становится дефицитным, и это приводит к снижению производительности для ǸI больше 4000.
  2. Этот ресурс не используется совместно с такими инструкциями, как xor - снижение производительности продолжается для NI около 4000, если jmp и xor выполняются друг за другом.
  3. Но этот ресурс используется совместно с je, если скачок сделан - на _29 _ + _ 30_ один за другим ресурс становится дефицитным на NI около 2000.
  4. Однако, если je вообще не прыгает, ресурс снова становится дефицитным, поскольку NI составляет около 4000 (4-я строка).

В статьях Мэтта Годболта о реверс-инжиниринге предсказания ветвлений установлено, что целевая буферная емкость ветвления составляет 4096 записи. Это очень убедительное свидетельство того, что промахи BTB являются причиной наблюдаемой разницы в пропускной способности между маленькими и большими jmp петлями.


person ead    schedule 07.08.2016    source источник
comment
Имена указаны в отладочной информации. У исполняемых файлов выпуска нигде не будет названий меток.   -  person doug65536    schedule 07.08.2016
comment
Обратите внимание, что xorq %rax,%rax делает то же самое, что и xorl %eax,%eax, поэтому почти никогда не бывает причин использовать первое (за исключением, возможно, того, чтобы избежать необходимости вставлять nop для выравнивания где-нибудь).   -  person fuz    schedule 07.08.2016
comment
Ваши большие 10000 циклов инструкций легко поместятся в кэш L2 современного процессора (256 КБ), поэтому вы не измеряете скорость ОЗУ.   -  person Ross Ridge    schedule 07.08.2016
comment
@RossRidge Вы правы, для mov и xor мне нужно пройти до 10 ^ 7 инструкций в цикле, чтобы увидеть скорость RAM. Однако jmp становится в 4 раза медленнее с 10 ^ 3 до 10 ^ 4. Я не говорю, что это из-за ОЗУ - это что-то другое, но я не совсем понимаю, что это такое.   -  person ead    schedule 07.08.2016
comment
Вы, вероятно, уже поняли это (поскольку вы написали этот тестовый пример в первую очередь), но, вероятно, он требует четкости - причина того, что ваш jmp+cmp+je (no jump) случай не затрагивает дефицит ресурсов до примерно 4000 прыжков, заключается в том, что прыжки, которые не выполняются, не выполняются t потребляют запись BTB (действительно, вставлять в BTB было бы нечего!).   -  person BeeOnRope    schedule 09.08.2016


Ответы (1)


TL: DR: я предполагаю, что заканчиваются записи BTB (целевой буфер ветвления). Конвейерная выборка кода должна предсказывать существование безусловного перехода еще до его декодирования. См. ниже.

Обновление 2021 года: https://blog.cloudflare.com/branch-predictor/ исследует это подробно, используя блок из jmp next_insn в качестве эксперимента. Например, могут иметь значение плотность ветвей и псевдонимы (одинаковое смещение относительно 64-байтовой строки).


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

Чтобы узнать больше о скачках и их влиянии на конвейерные ЦП, Управляйте опасностями в классическом конвейере RISC должно быть хорошим введением к тому, почему ветвления трудны для конвейерных процессоров. Руководства Агнера Фога объясняют практические последствия, но я думаю, что предполагаю наличие некоторых базовых знаний такого рода.


Ваш процессор Intel Broadwell имеет uop-cache, который кэширует декодированные инструкции (отдельно от кэш-память L1 32kiB).

Размер кэша мопов составляет 32 набора из 8 способов, по 6 мопов на строку, всего 1536 мопов (если каждая строка упакована по 6 мопов; отличная эффективность). 1536 мопс - это между вашими 1000 и 10000 размерами теста. Перед вашим редактированием я предсказал, что отсечка от медленного к быстрому будет примерно 1536 инструкций в вашем цикле. Он вообще не замедляется до тех пор, пока не превысит 1536 инструкций, поэтому я думаю, что мы можем исключить эффекты uop-cache. Это не такой простой вопрос, как я думал. :)

Запуск из uop-cache (небольшой размер кода) вместо декодеров инструкций x86 (большие циклы) означает, что меньше этапов конвейера перед этапом, который распознает jmp инструкции. Таким образом, мы можем ожидать, что пузыри от постоянного потока скачков будут меньше, даже если они предсказаны правильно.

Предполагается, что запуск от декодеров приведет к большему штрафу за неверное предсказание ветвления (например, может быть 20 циклов вместо 15), но это не неверно предсказанные ветвления.


Даже несмотря на то, что ЦП не нужно предсказывать, взято ли ветвление или нет, он по-прежнему использует ресурсы предсказания ветвления, чтобы предсказать, что блок кода содержит взятую ветвь, прежде чем он будет декодирован.

Кэширование того факта, что в определенном блоке кода есть ветвь и его целевой адрес, позволяет интерфейсу начать выборку кода из цели ветвления до фактического декодирования кодировки jmp rel32. Помните, что декодировать инструкции x86 переменной длины сложно: вы не знаете, где начинается одна инструкция, пока не будет декодирована предыдущая. Таким образом, вы не можете просто сопоставить поток инструкций по шаблону, ища безусловные переходы / вызовы, как только он будет получен.

Моя текущая теория заключается в том, что вы замедляетесь, когда у вас заканчиваются записи Branch-target-buffer.

См. Также Какое неверное предсказание ветвления обнаруживает целевой буфер ветвления?, у которого есть хороший ответ и обсуждение в этой ветке Realworldtech.

Один очень важный момент: BTB предсказывает, какой блок будет извлекаться следующим, а не точное место назначения конкретной ветви в блоке выборки. Поэтому вместо того, чтобы прогнозировать цели для всех ветвей в блоке выборки, просто необходимо чтобы предсказать адрес следующей выборки.


Да, пропускная способность памяти может быть узким местом при выполнении таких операций с очень высокой пропускной способностью, как xor-zeroing, но с jmp вы сталкиваетесь с другим узким местом. ЦП успеет извлечь 42 Б из памяти, но это не то, что он делает. Предварительная выборка может легко справиться с 2 байтами за 3 такта, поэтому пропусков I-кеша L1 должно быть почти нулевым.

В вашем xor с / без REX-теста пропускная способность основной памяти могла быть узким местом, если вы тестировали с достаточно большим циклом, который не помещался в кэш L3. Я потребляю 4 * 2 Байт за цикл на процессоре с тактовой частотой ~ 3 ГГц, что примерно соответствует максимальной скорости 25 Гбайт / с DDR3-1600 МГц. Тем не менее, даже кэш L3 будет достаточно быстрым, чтобы не отставать от 4 * 3Б за цикл.

Что интересно, узким местом является BW основной памяти; Сначала я предположил, что декодирование (блоками по 16 байт) будет узким местом для 3-байтовых операций XOR, но я думаю, что они достаточно малы.


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

Для сравнения тактовых циклов используйте perf stat ./a.out. Существуют и другие полезные счетчики производительности, которые необходимы для понимания характеристик производительности.

См. x86-64 Относительная производительность jmp для результатов счетчика производительности из Core2. (8 циклов на jmp) и какая-то неизвестная микроархитектура, где это ~ 10c на jmp.


Подробности характеристик производительности современных ЦП достаточно сложно понять даже в более или менее условиях белого ящика (читая руководство Intel по оптимизации и то, что они опубликовали о внутреннем устройстве ЦП). Вы рано и часто застрянете, если будете настаивать на тестировании черного ящика, когда вы не читаете такие вещи, как статьи arstechnica о новом дизайне процессора, или, может быть, некоторые более подробные вещи, такие как Обзор микроархитектуры Haswell или аналогичную статью о Sandybridge, на которую я ссылался ранее.

Если застревать рано и часто - это нормально, и вам весело, то во что бы то ни стало продолжайте делать то, что делаете. Но людям труднее отвечать на ваши вопросы, если вы не знаете этих деталей, как в этом случае. : / например моя первая версия этого ответа предполагала, что вы прочитали достаточно, чтобы узнать, что такое кеш uop.

person Peter Cordes    schedule 07.08.2016
comment
Спасибо за ваш ответ. Я не совсем уверен, что вы имеете в виду под uop-cache: operation-cache (который должен быть 32 КБ на моей машине i-7) или prefetch-queue (я предполагаю, что у моей машины она есть, не знаю, насколько большой)? - person ead; 07.08.2016
comment
В моем случае jmp - это всего лишь 2 байта nop. Нет необходимости загружать новую операцию в очередь предварительной выборки, поэтому я не уверен, что пузыри являются причиной медлительности. Эти пузыри также могут стать проблемой для кода меньшего размера, но это не так. - person ead; 07.08.2016
comment
Как вы сказали, ОЗУ здесь не является ограничивающим фактором, потому что за одну операцию загружается только 2 байта. Правильно ли я понимаю, что ваше предположение состоит в том, что расшифровка инструкции jmp может быть здесь узким местом? - person ead; 07.08.2016
comment
Я пришел к выводу, что узким местом для программ больших размеров является ОЗУ, потому что время выполнения (таких инструкций, как xor, mov, add) было пропорционально размеру инструкции, а скорость была почти точной скоростью моей ОЗУ ( всего около 10 ГБ / с). Это моя причина сказать, что 4,2 нс будет достаточно для чтения 42 байтов из памяти. - person ead; 07.08.2016
comment
@ead Кэш µop кеширует декодированные инструкции, то есть это кеш микрокода. Я думаю, что это 27 или около того длины. Если ваш цикл достаточно плотный, чтобы поместиться в кэш µop, ЦП не должен запускать декодер при последующих итерациях цикла. - person fuz; 07.08.2016
comment
@FUZxxl, спасибо, мой жесткий цикл имеет 1000 прыжков, а мой большой - около 10000, поэтому я думаю, что кеш микрокода не должен иметь никакого значения. - person ead; 07.08.2016
comment
@ead (и FUZxxl): uop cache в процессорах семейства Intel Sandybridge кеш-декодированные инструкции, но он намного больше, чем буфер цикла. Он вмещает не более 1536 мопов, в зависимости от того, насколько хорошо мопы упаковываются в строки кэша группами по 6. Я не помню, каковы правила для взятых ветвей, заканчивающихся строками кэша мопов. Агнер Фог исследовал некоторые из них. Ваше тестирование потенциально свидетельствует о том, что несколько jmp мопов могут поместиться в одной строке кэша. Было бы здорово, если бы вы нашли размер отсечки между медленным и быстрым. Я предсказываю ~ 1536 jmpсек. - person Peter Cordes; 07.08.2016
comment
@ead: В моем случае jmp - это всего лишь 2 байта nop: да, но ЦП не имеет никаких оптимизаций для этого бесполезного особого случая. Он по-прежнему запускает его как обычный jmp, который требует перезапуска команды извлечения + декодирования из нового места. - person Peter Cordes; 07.08.2016
comment
@FUZxxl: Вы говорите о буфере цикла, где ЦП повторно использует мопы в очереди декодированных инструкций 28 моп вместо повторного декодирования или повторной выборки из кэша мопов. Nehalem представил это, но основное изменение архитектуры SnB добавило целый кэш uop (при сохранении обычного I-кеша L1, в отличие от P4). - person Peter Cordes; 07.08.2016
comment
@ead: Я сделал большую правку после того, как перечитал ваш вопрос и выяснил, что вы действительно тестируете это в черном ящике, и не очень много читал о том, как работает ваш процессор. Я бы не стал этим заниматься, но я думаю, что это действенный и потенциально интересный подход. Сначала я подумал, что вы прочитали материал и просто тестируете затраты на обучение, а не пытаетесь выяснить все остальное одновременно! - person Peter Cordes; 07.08.2016
comment
Большое спасибо за ваше время и ваши ответы - мы очень ценим это! Мне просто нужно время, чтобы просмотреть статью, чтобы понять ваши рассуждения. - person ead; 07.08.2016
comment
Моя машина имеет архитектуру Broadwell. Ваше предположение о 1536uops неверно (см. Мое редактирование), но, возможно, у Broadwell есть больший кеш uop ... - person ead; 07.08.2016
comment
@ead: Спасибо за тестирование, теперь становится интересно :). AFAIK, размер кэша uop не изменился с Sandybridge даже до Skylake. Это по-прежнему 32 набора по 8 линий, по 6 мопов на линию, что в сумме составляет 1,5 тыс. Мопов (если каждая линия заполнена 6 мопами: идеальная эффективность). Никакого замедления до тех пор, пока не превысит 2k insns, определенно исключает кеш uop. Я не уверен, что это еще может быть, поскольку он все еще намного меньше, чем I-cache L1. Может быть, какие-то ресурсы предсказания ветвлений? Возможно, записи в целевом буфере ветвления используются для ускорения безусловного jmp? - person Peter Cordes; 07.08.2016
comment
@ead: у меня есть новое предположение, см. мое редактирование записей BTB. На самом деле у меня нет доступа к оборудованию SnB для тестирования, иначе я бы попробовал это сам и посмотрел на счетчики производительности. - person Peter Cordes; 07.08.2016
comment
Я добавил экспериментальные результаты, которые подтверждают ваше предположение о TBT. Излишне говорить, что без вас я бы никогда этого не узнал. - person ead; 08.08.2016
comment
@ead: BTB, а не TBT. (Эта опечатка присутствует и в вашем вопросе). Это круто; Я не был уверен, какие именно ресурсы израсходованы безусловными jmps. Я бы не знал, чего ожидать от тестов, которые вы сделали, так что это определенно интересно. Я собирался предложить добавить NOPs или что-то еще, чтобы исключить чистые эффекты размера кода, но вы уже подумали об этом к тому времени, когда я пришел, чтобы опубликовать комментарий. Добавление неиспользованных условных ветвей было отличной идеей. - person Peter Cordes; 08.08.2016
comment
Да, у вас в основном есть два отдельных ресурса прогнозирования ветвлений на современных ЦП - хорошо известный предсказатель направления ветвления, необходимый для принятого или не принятого решения по условным переходам, и BTB. Второй из этих ресурсов ветки необходим для всех типов переходов, которые когда-либо выполнялись, включая все безусловные переходы, такие как jmp или call, а также условные переходы и косвенные переходы. Даже если цель ветвления является константой, в конвейере декодирования нет волшебства, которое позволило бы клиентской части перенаправить на место перехода - он полагается на BTB. - person BeeOnRope; 09.08.2016
comment
@BeeOnRope: Действительно ли высокопроизводительные реализации ISA с фиксированным размером insn, такие как PPC или ARM64 (т.е. не Thumb2), действительно сканируют поток insn на предмет прямых переходов на очень ранней стадии конвейера (перед обычным декодированием)? Я бы так предположил, но я думаю, что все еще не удается избежать пузырей с доставкой. Это, безусловно, сократит пузыри для случаев, когда BTB холодный, по сравнению с тем, что может сделать x86. - person Peter Cordes; 09.08.2016
comment
Я не разработчик оборудования, но подозреваю, что нет - поскольку кажется, что BTB очень хорошо выполняет эту функцию - это происходит до любого вида декодирования вообще, основанного только на IP, и поэтому должно быть самым быстрым (избегая пузырей ) подход. Недостатками являются (1) дополнительное использование BTB, но другой подход имеет дополнительную сложность, поскольку вместо этого вы могли бы просто использовать больше ресурсов BTB и (2) холодный случай, но этот случай кажется ограниченным по влиянию, за исключением крайних случаев (например, цикл огромных чисел с большим количеством переходов, чем записей BTB). - person BeeOnRope; 09.08.2016
comment
@BeeOnRope: Я имел в виду, что они могут делать и то, и другое: без пузырей благодаря BTB в горячем случае, более коротких пузырей из-за раннего сканирования в холодном случае. Хотя это требует затрат энергии и транзисторов и, вероятно, не очень помогает для многих рабочих нагрузок высокопроизводительных вычислений. Более полезно для запуска раздуваемого современного программного обеспечения с графическим интерфейсом, такого как веб-браузеры. Мне было бы интересно узнать, каковы цифры для этого компромисса по сравнению с другими вещами, на которые PowerPC или ARM могут потратить бюджет мощности. Я думаю, что это тот пузырь, для которого SMT идеально подходит. - person Peter Cordes; 09.08.2016
comment
Да, в этом есть смысл. Я попросил экспертов здесь взвесить свое мнение. точечные ответвления будут обнаружены, и выборка будет изменена, но я думаю, ваш вопрос в том, как рано? Может быть, даже до декодирования (ваша первоначальная идея)? Если нет, это при декодировании / вокруг? Или он должен полностью ждать выполнения (то есть так же плохо, как неверное предсказание ветки)? - person BeeOnRope; 09.08.2016