Выравнивание ветвей для циклов с использованием микрокодированных инструкций для процессоров Intel SnB-семейства

Это связано, но не то же самое, что и этот вопрос: Оптимизация производительности сборки x86-64 - Согласование и предсказание ветвей и немного связано с моим предыдущим вопросом: Неподписанное 64-битное преобразование: почему этот алгоритм из g++

Ниже приведен тестовый пример не в реальном мире. Этот алгоритм тестирования примитивности не является разумным. Я подозреваю, что любой алгоритм реального мира никогда не будет выполнять такой маленький внутренний цикл довольно много раз (num - это просто размер около 2 ** 50). В С++ 11:

using nt = unsigned long long;
bool is_prime_float(nt num)
{
   for (nt n=2; n<=sqrt(num); ++n) {
      if ( (num%n)==0 ) { return false; }
   }
   return true;
}

Затем g++ -std=c++11 -O3 -S выдает следующее: RCX содержит n и XMM6, содержащие sqrt(num). См. Мой предыдущий пост для оставшегося кода (который никогда не выполняется в этом примере, поскольку RCX никогда не становится достаточно большим, чтобы рассматриваться как подписанный отрицательный результат).

jmp .L20
.p2align 4,,10
.L37:
pxor    %xmm0, %xmm0
cvtsi2sdq   %rcx, %xmm0
ucomisd %xmm0, %xmm6
jb  .L36   // Exit the loop
.L20:
xorl    %edx, %edx
movq    %rbx, %rax
divq    %rcx
testq   %rdx, %rdx
je  .L30   // Failed divisibility test
addq    $1, %rcx
jns .L37
// Further code to deal with case when ucomisd can't be used

Я использую это время, используя std::chrono::steady_clock. Я продолжал получать странные изменения производительности: просто добавляя или удаляя другой код. В конечном итоге я отследил это до вопроса о выравнивании. Команда .p2align 4,,10 пыталась выровнять по границе 2 ** 4 = 16 байтов, но для этого используется не более 10 байтов заполнения, я думаю, чтобы сбалансировать выравнивание и размер кода.

Я написал Python script, чтобы заменить .p2align 4,,10 на ручное число команд nop. Следующий график рассеяния показывает самые быстрые 15 из 20 прогонов, время в секундах, количество отступов байтов по оси x:

Scatter plot

Из objdump без заполнения, команда pxor будет выполняться со смещением 0x402f5f. Запуск на ноутбуке, Sandybridge i5-3210m, turboboost отключен, я обнаружил, что

  • Для заполнения 0 байтов, низкой производительности (0,42 с)
  • Для заполнения 1-4 байта (смещение 0x402f60 до 0x402f63) получается немного лучше (0,41 с, видимое на графике).
  • Для заполнения 5-20 байт (смещение 0x402f64 до 0x402f73) получают быструю производительность (0,37 с)
  • Заполнение от 21 до 32 байт (смещение 0x402f74 до 0x402f7f) медленная производительность (0,42 с)
  • Затем циклы по 32-байтовому образцу

Таким образом, выравнивание по 16 байт не дает лучшей производительности - оно помещает нас в немного лучшую (или только меньшую вариацию, из области разброса). Выравнивание 32 плюс 4-19 дает наилучшую производительность.

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

Я не вижу проблем с прогнозированием ветвлений. Может ли это быть quopk кэша uop?

Изменив алгоритм С++ на кеширование sqrt(num) в 64-битовом целое, а затем сделайте цикл чисто целочисленным, я удалю проблему - выравнивание теперь не имеет никакого значения.

Ответ 1

Вот что я нашел на Skylake для того же цикла. Весь код для воспроизведения моих тестов на вашем оборудовании находится в github.

Я наблюдаю три разных уровня производительности, основанных на выравнивании, тогда как OP действительно видел только 2 первичных. Уровни очень четкие и повторяемые 2:

введите описание изображения здесь

Мы видим три различных уровня производительности здесь (образец повторяется, начиная со смещения 32), который мы будем называть регионами 1, 2 и 3 слева направо (область 2 разделена на две части, охватывающие область 3). Самая быстрая область (1) - от 0 до 8, средняя (2) - от 9-18 и 28-31, а самая медленная (3) - от 19-27. Разница между каждой областью близка или равна 1 циклу/итерации.

На основе счетчиков производительности самая быстрая область сильно отличается от двух других:

  • Все инструкции доставляются из унаследованного декодера, а не из DSB 1.
  • Для каждой итерации цикла существует ровно 2 декодера с декодером ↔ (idq_ms_switches).

На руке две более медленные области довольно схожи:

  • Все инструкции доставляются из DSB (uop cache), а не из унаследованного декодера.
  • На итерацию цикла имеется ровно 3 декодера и микрокода.

Переход от самой быстрой к средней области, так как смещение изменяется от 8 до 9, точно соответствует тому, когда цикл начинает подходить в буфере uop из-за проблем с выравниванием. Вы считаете это так же, как Петр в своем ответе:

Смещение 8:

  LSD? <_start.L37>:
  ab 1 4000a8:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000ac:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000b1:  66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1 4000b5:  72 21                   jb     4000d8 <_start.L36>
  ab 2 4000b7:  31 d2                   xor    edx,edx
  ab 2 4000b9:  48 89 d8                mov    rax,rbx
  ab 3 4000bc:  48 f7 f1                div    rcx
  !!!! 4000bf:  48 85 d2                test   rdx,rdx
       4000c2:  74 0d                   je     4000d1 <_start.L30>
       4000c4:  48 83 c1 01             add    rcx,0x1
       4000c8:  79 de                   jns    4000a8 <_start.L37>

В первом столбце я аннотировал, как uops для каждой команды заканчиваются в кэше uop. "ab 1" означает, что они входят в набор, связанный с адресом типа ...???a? или ...???b? (каждый набор охватывает 32 байта, aka 0x20), а 1 означает способ 1 (из максимум 3).

В точке!!! этот бюст выходит из кэша uop, потому что команда test не имеет места для перемещения, все три способа используются.

С другой стороны, посмотрим на смещение 9:

00000000004000a9 <_start.L37>:
  ab 1 4000a9:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000ad:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000b2:  66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1 4000b6:  72 21                   jb     4000d9 <_start.L36>
  ab 2 4000b8:  31 d2                   xor    edx,edx
  ab 2 4000ba:  48 89 d8                mov    rax,rbx
  ab 3 4000bd:  48 f7 f1                div    rcx
  cd 1 4000c0:  48 85 d2                test   rdx,rdx
  cd 1 4000c3:  74 0d                   je     4000d2 <_start.L30>
  cd 1 4000c5:  48 83 c1 01             add    rcx,0x1
  cd 1 4000c9:  79 de                   jns    4000a9 <_start.L37>

Теперь нет проблем! Команда test проскользнула в следующую строку 32B (строка cd), поэтому все вписывается в кеш-память.

Итак, это объясняет, почему в этот момент происходит изменение между MITE и DSB. Однако он не объясняет, почему путь MITE быстрее. Я пробовал несколько простых тестов с div в цикле, и вы можете воспроизвести это с помощью более простых циклов без каких-либо вещей с плавающей запятой. Это странно и чувствительно к случайным другим вещам, которые вы вставляете в цикл.

Например, этот цикл также выполняется быстрее из унаследованного декодера, чем DSB:

ALIGN 32
    <add some nops here to swtich between DSB and MITE>
.top:
    add r8, r9
    xor eax, eax
    div rbx
    xor edx, edx
    times 5 add eax, eax
    dec rcx
    jnz .top

В этом цикле добавление бессмысленной команды add r8, r9, которая на самом деле не взаимодействует с остальной частью цикла, ускорила работу над версией MITE (но не с версией DSB).

Итак, я думаю, что разница между областью 1 и регионом 2 и 3 связана с тем, что предыдущий выполняет вне наследственного декодера (что, как ни странно, делает его быстрее).


Посмотрим также на смещение 18 для смещения 19 перехода (где область 2 заканчивается и 3 начинается):

Смещение 18:

00000000004000b2 <_start.L37>:
  ab 1 4000b2:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1  4000b6: f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1  4000bb: 66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1  4000bf: 72 21                   jb     4000e2 <_start.L36>
  cd 1  4000c1: 31 d2                   xor    edx,edx
  cd 1  4000c3: 48 89 d8                mov    rax,rbx
  cd 2  4000c6: 48 f7 f1                div    rcx
  cd 3  4000c9: 48 85 d2                test   rdx,rdx
  cd 3  4000cc: 74 0d                   je     4000db <_start.L30>
  cd 3  4000ce: 48 83 c1 01             add    rcx,0x1
  cd 3  4000d2: 79 de                   jns    4000b2 <_start.L37>

Смещение 19:

00000000004000b3 <_start.L37>:
  ab 1 4000b3:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000b7:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000bc:  66 0f 2e f0             ucomisd xmm6,xmm0
  cd 1 4000c0:  72 21                   jb     4000e3 <_start.L36>
  cd 1 4000c2:  31 d2                   xor    edx,edx
  cd 1 4000c4:  48 89 d8                mov    rax,rbx
  cd 2 4000c7:  48 f7 f1                div    rcx
  cd 3 4000ca:  48 85 d2                test   rdx,rdx
  cd 3 4000cd:  74 0d                   je     4000dc <_start.L30>
  cd 3 4000cf:  48 83 c1 01             add    rcx,0x1
  cd 3 4000d3:  79 de                   jns    4000b3 <_start.L37>

Единственное отличие, которое я вижу здесь, это то, что первые 4 команды в случае смещения 18 вписываются в строку кэша ab, но только 3 в случае сдвига 19. Если мы предположим, что DSB может только доставить uops в IDQ из одного набора кешей, это означает, что в какой-то момент может быть выпущен один uop и выполнить цикл раньше в сценарии смещения 18, чем в сценарии 19 (представьте, например, что IDQ пуст). В зависимости от того, к какому порту относится uop в контексте окружающего потока uop, это может задержать цикл за один цикл. Действительно, разница между областью 2 и 3 составляет ~ 1 цикл (в пределах погрешности).

Итак, я думаю, мы можем сказать, что разница между 2 и 3, скорее всего, обусловлена ​​выравниванием кеша uop - область 2 имеет немного лучшее выравнивание, чем 3, с точки зрения выпуска одного дополнительного uop на один цикл раньше.


Некоторые примечания о дополнениях, которые я проверил, которые не выходили за рамки возможной причины замедления:

  • Несмотря на режимы DSB (регионы 2 и 3), имеющие 3 микрокодовых переключателя в сравнении с 2 пути MITE (область 1), это, по-видимому, не приводит к замедлению. В частности, более простые циклы с div выполняются в одинаковых количествах циклов, но все же показывают 3 и 2 переключателя для путей DSB и MITE соответственно. Так что нормальный и прямо не означает замедление.

  • Оба пути выполняют по существу одинаковое количество uops и, в частности, имеют одинаковое количество uops, сгенерированных секвенсором микрокода. Таким образом, это не похоже на то, что в разных регионах выполняется более общая работа.

  • На самом деле не было разницы в пропущенных кешках (очень низких, как ожидалось) на разных уровнях, неверных предсказаниях отрасли (в основном нулевых 3) или любых других типах штрафов или необычных условия, которые я проверил.

Что принесло плоды, он смотрит на образец использования единицы исполнения в разных регионах. Здесь рассмотрим распределение uops, выполняемых за цикл, и некоторые показатели стойкости:

+----------------------------+----------+----------+----------+
|                            | Region 1 | Region 2 | Region 3 |
+----------------------------+----------+----------+----------+
| cycles:                    | 7.7e8    | 8.0e8    | 8.3e8    |
| uops_executed_stall_cycles | 18%      | 24%      | 23%      |
| exe_activity_1_ports_util  | 31%      | 22%      | 27%      |
| exe_activity_2_ports_util  | 29%      | 31%      | 28%      |
| exe_activity_3_ports_util  | 12%      | 19%      | 19%      |
| exe_activity_4_ports_util  | 10%      | 4%       | 3%       |
+----------------------------+----------+----------+----------+

Я выбрал несколько разных значений смещения, и результаты были согласованными в каждом регионе, но между регионами у вас разные результаты. В частности, в области 1 у вас меньше циклов остановки (циклы, в которых не выполняется uop). У вас также есть значительные различия в циклах без остановок, хотя четкая "лучшая" или "худшая" тенденция не проявляется. Например, в области 1 есть еще много циклов (10% против 3% или 4%) с выполненными 4 uops, но другие регионы в значительной степени компенсируют это с большим количеством циклов с тремя выполненными uops и несколькими циклами с 1 uop выполненным.

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

Посмотрим, что toplev.py должно сказать об этом... (результаты опущены).

Хорошо, toplev предполагает, что основным узким местом является front-end (50 +%). Я не думаю, что вы можете доверять этому, потому что способ вычисления FE-привязки кажется сломанным в случае длинных строк микрокодированных инструкций. FE-привязка основана на frontend_retired.latency_ge_8, которая определяется как:

Отставные инструкции, которые извлекаются после интервала, где front-end не отправил ни одного софта в течение 8 циклов, что не было прерванный внутренним киоском. (Поддерживает PEBS)

Обычно это имеет смысл. Вы считаете инструкции, которые были отложены, потому что интерфейс не выполнял циклы. Условие "не прерывается с помощью back-end stall" гарантирует, что это не будет срабатывать, когда интерфейс не будет отправлен на работу только потому, что бэкэнд не может их принять (например, когда RS заполнен, бэкэнд выполняет некоторые команды низкого уровня).

Кажется, что для инструкций div - даже простой цикл с почти одним только div показывает:

FE      Frontend_Bound:                57.59 %           [100.00%]
BAD     Bad_Speculation:                0.01 %below      [100.00%]
BE      Backend_Bound:                  0.11 %below      [100.00%]
RET     Retiring:                      42.28 %below      [100.00%]

То есть единственным узким местом является интерфейс ( "увольнение" не является узким местом, оно представляет собой полезную работу). Очевидно, что такая петля тривиально обрабатывается интерфейсом и вместо этого ограничивается способностью перехватывать все команды, генерируемые операцией div. Toplev может получить это действительно неправильно, потому что (1) может быть, что uops, поставленные секвенсором микрокода, не учитываются в счетчиках frontend_retired.latency..., так что каждая операция div заставляет это событие подсчитывать все последующие инструкции (даже хотя CPU был занят в течение этого периода - не было реального срыва), или (2) секвенсор микрокода мог бы доставить все свои взлеты, по существу, "вверх", захлопывая ~ 36 ударов в IDQ, после чего он не доставляет уже до тех пор, пока div не закончится или что-то в этом роде.

Тем не менее, мы можем посмотреть на нижние уровни toplev для подсказок:

Основное различие между вызовами между регионами 1 и регионом 2 и 3 - увеличение штрафа ms_switches для последних двух регионов (поскольку они несут 3 каждой итерации против 2 для устаревшего пути. Внутренне toplev оценивает штраф в 2 цикла в интерфейсе для таких переключателей. Конечно, независимо от того, что эти штрафы фактически замедляют что-либо, зависит сложным образом от очереди команд и других факторов. Как упоминалось выше, простой цикл с div не показать любую разницу между путями DSB и MITE, цикл с дополнительными инструкциями. Таким образом, может быть, что дополнительный пузырь переключателя поглощается в более простых циклах (где основной обработкой всех uops, генерируемых div, является основным фактором), но как только вы добавите какую-то другую работу в цикл, коммутаторы станут фактором, по крайней мере, для периода перехода между работами div и non-div`.

Итак, я предполагаю, что мой вывод состоит в том, что способ, которым команда div взаимодействует с остальной частью внешнего потока uop и выполнением бэкэнд, не совсем понятен. Мы знаем, что это связано с потоком uops, который поставляется как из MITE/DSB (кажется, 4 uops per div), так и из секвенсора микрокода (похоже, ~ 32 uops per div, хотя он изменяется с разными входными значениями div op) - но мы не знаем, что такое эти uops (мы можем видеть их распределение портов, хотя). Все это делает поведение довольно непрозрачным, но я думаю, что это, вероятно, до тех пор, пока MS-переключатели не будут закрыты передним концом или небольшие различия в потоке доставки uop, что приведет к различным решениям планирования, которые в конечном итоге станут мастером заказа MITE.


1 Конечно, большинство из них не доставляются из унаследованного декодера или DSB вообще, а через микрокод-секвенсор (мс). Поэтому мы свободно говорим о инструкциях, а не об ошибках.

2 Обратите внимание, что ось x здесь является "смещенным байтом от 32B-выравнивания". То есть 0 означает, что вершина цикла (метка .L37) выровнена с границей 32B, а 5 означает, что цикл начинается на пять байт ниже границы 32B (используя nop для заполнения) и так далее. Таким образом, мои байты заполнения и смещение одинаковы. ОП использовал другое значение для смещения, если я правильно понял: его 1 байт заполнения заполнил 0 смещение. Таким образом, вы должны вычесть 1 из значений дополнения OPs, чтобы получить значения смещения.

3 Фактически, скорость предсказания ветвления для типичного теста с prime=1000000000000037 составляла ~ 99,999997%, отражая только 3 неверно предсказанных ветвления за весь прогон (вероятно, на первом проходе через цикл, и последняя итерация).

4 UPC, т.е. за каждый цикл - мера, тесно связанная с IPC для аналогичных программ, а другая - немного более точная, когда мы подробно рассмотрим потоки uop. В этом случае мы уже знаем, что значения uop одинаковы для всех вариантов выравнивания, поэтому UPC и IPC будут прямо пропорциональными.

Ответ 2

У меня нет конкретного ответа, всего несколько гипотез, которые я не могу проверить (отсутствие аппаратного обеспечения). Я думал, что нашел что-то убедительное, но у меня было выравнивание на единицу (потому что вопрос учитывает прокладку от 0x5F, а не от выровненной границы). В любом случае, мы надеемся, что это полезно, чтобы опубликовать это в любом случае, чтобы описать факторы, которые, вероятно, здесь играют.

В вопросе также не указывается кодировка ветвей (короткий (2B) или рядом (6B)). Это оставляет слишком много возможностей для изучения и теоретики о том, какая именно инструкция пересекает границу 32B или нет, вызывает проблему.


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


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

Вы можете ожидать, что цикл, который является узким местом при разделении, не будет узким местом на интерфейсе или не будет затронут выравниванием, потому что деление медленное, а цикл работает очень мало инструкций за такт. Этот истинный, но 64-разрядный DIV микрокодирован как 35-57 микроопераций (IOP) на IvyBridge, так что, оказывается, могут быть проблемы с интерфейсом.

Два основных способа выравнивания могут иметь значение:

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

Ваш процессор Intel IvyBridge - это матрица SandyBridge. Он имеет несколько изменений (например, удаление mov и ERMSB), но интерфейс аналогичен между SnB/IvB/Haswell. Agar Fog microarch pdf содержит достаточно подробностей для анализа того, что должно произойти, когда процессор запускает этот код. См. Также David Kanter Запись SandyBridge для блок-диаграммы этапов выборки/декодирования, но он разделяет выборку/декодирование из кэша uop, микрокода, и декодированная-uop очередь. В конце есть полная блок-схема целого ядра. Его статья Хасуэлла содержит блок-диаграмму, включающую весь интерфейс, вплоть до очереди декодированных -упов, которая передает этап выпуска. (IvyBridge, как и Хасуэлл, имеет буфер с буфером/петлей в 56 часов, когда не использует Hyperthreading. Sandybridge статически разделяет их на очереди 2x28 uop, даже когда HT отключен.)

David Kanter's SnB writeup

Изображение скопировано из Дэвид Кэнтер также отлично справляется с работой Haswell, где он включает декодеры и uop-cache на одной диаграмме.

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

В соответствии с руководством по оптимизации Intel (2.3.2.2 Decoded ICache):

  • Все микрооперации в пути (строка кэша uop) представляют собой команды, которые являются статически смежными в коде и имеют их EIP в том же выровненном 32-байтовом регионе. (Я думаю, что это означает, что инструкция, которая проходит за границей, проходит в кэше uop для блока, содержащего его начало, а не в конце. Инструкции по развертыванию должны идти куда-то, а целевой адрес ветки, который будет запускать инструкцию, - это начало insn, поэтому наиболее полезно поместить его в строку для этого блока).
  • Команда multi micro-op не может быть разделена между путями.
  • Инструкция, которая включает MSROM, использует весь путь. (т.е. любая команда, которая занимает более 4-х часов (для reg, reg form), является микрокодированной. Например, DPPD не является микрокодированным (4 раза), но DPPS - это (6 uops). DPPD с операндом памяти, который может микро-предохранитель будет состоять из 5-ти томов, но все равно не потребуется включать секвенсор микрокода (не тестировался).
  • Допускается использование до двух ветвей.
  • Пара макроконфигурированных инструкций хранится как один микрооператор.

Дэвид Кантер В записи SnB есть еще подробные сведения о кэше uop.


Посмотрите, как фактический код войдет в кеш-память u/

# let consider the case where this is 32B-aligned, so it runs in 0.41s
# i.e. this is at 0x402f60, instead of 0 like this objdump -Mintel -d output on a  .o
# branch displacements are all 00, and I forgot to put in dummy labels, so they're using the rel32 encoding not rel8.

0000000000000000 <.text>:
   0:   66 0f ef c0             pxor   xmm0,xmm0    # 1 uop
   4:   f2 48 0f 2a c1          cvtsi2sd xmm0,rcx   # 2 uops
   9:   66 0f 2e f0             ucomisd xmm6,xmm0   # 2 uops
   d:   0f 82 00 00 00 00       jb     0x13         # 1 uop  (end of one uop cache line of 6 uops)

  13:   31 d2                   xor    edx,edx      # 1 uop
  15:   48 89 d8                mov    rax,rbx      # 1 uop  (end of a uop cache line: next insn doesn't fit)

  18:   48 f7 f1                div    rcx          # microcoded: fills a whole uop cache line.  (And generates 35-57 uops)

  1b:   48 85 d2                test   rdx,rdx      ### PROBLEM!!  only 3 uop cache lines can map to the same 32-byte block of x86 instructions.
  # So the whole block has to be re-decoded by the legacy decoders every time, because it doesn't fit in the uop-cache
  1e:   0f 84 00 00 00 00       je     0x24         ## spans a 32B boundary, so I think it goes with TEST in the line that includes the first byte.  Should actually macro-fuse.
  24:   48 83 c1 01             add    rcx,0x1      # 1 uop 
  28:   79 d6                   jns    0x0          # 1 uop

Итак, с выравниванием 32B для начала цикла, он должен запускаться из устаревших декодеров, что потенциально медленнее, чем запуск из кэша uop. При переключении с uop-кэша на устаревшие декодеры могут возникнуть некоторые накладные расходы.

@Тестирование (см. комментарии к вопросу) показывает, что любая микрокодированная команда предотвращает запуск цикла из буфера обратной связи. См. Комментарии к вопросу. (LSD = Loop Stream Detector = буфер буфера, физически та же структура, что и IDQ (очередь декодирования команд). DSB = Decode Stream Buffer = uop cache. MITE = устаревшие декодеры.)

Прерывание кэша uop может повредить производительность, даже если цикл достаточно мал для запуска из LSD (минимум 28 мкс или 56 без гиперпотока на IvB и Haswell).

Руководство по оптимизации Intel (раздел 2.3.2.4) говорит, что требования LSD включают

  • Все микрооперации также находятся в декодированном ICache.

Итак, это объясняет, почему микрокод не квалифицируется: в этом случае uop-cache содержит только указатель на микрокод, а не сами юпики. Также обратите внимание, что это означает, что перебор кэша uop по любой другой причине (например, много однобайтовых инструкций NOP) означает, что цикл не может работать из LSD.


С минимальное дополнение для быстрого, согласно тестированию OP.

# branch displacements are still 32-bit, except the loop branch.
# This may not be accurate, since the question didn't give raw instruction dumps.
# the version with short jumps looks even more unlikely

0000000000000000 <loop_start-0x64>:
    ...
  5c:   00 00                   add    BYTE PTR [rax],al
  5e:   90                      nop
  5f:   90                      nop

  60:   90                      nop         # 4NOPs of padding is just enough to bust the uop cache before (instead of after) div, if they have to go in the uop cache.
          # But that makes little sense, because looking backward should be impossible (insn start ambiguity), and we jump into the loop so the NOPs don't even run once.
  61:   90                      nop
  62:   90                      nop
  63:   90                      nop

0000000000000064 <loop_start>:                   #uops #decode in cycle A..E
  64:   66 0f ef c0             pxor   xmm0,xmm0   #1   A
  68:   f2 48 0f 2a c1          cvtsi2sd xmm0,rcx  #2   B
  6d:   66 0f 2e f0             ucomisd xmm6,xmm0  #2   C (crosses 16B boundary)
  71:   0f 82 db 00 00 00       jb     152         #1   C

  77:   31 d2                   xor    edx,edx     #1   C
  79:   48 89 d8                mov    rax,rbx     #1   C

  7c:   48 f7 f1                div    rcx       #line  D

  # 64B boundary after the REX in next insn    
  7f:   48 85 d2                test   rdx,rdx     #1   E
  82:   74 06                   je     8a <loop_start+0x26>#1 E
  84:   48 83 c1 01             add    rcx,0x1     #1   E
  88:   79 da                   jns    64 <loop_start>#1 E

Префикс REX test rdx,rdx находится в том же блоке, что и DIV, поэтому он должен испортить кеш-память. Еще один байт дополнения добавит его в следующий блок 32B, что будет иметь смысл. Возможно, результаты OP неправильны или, возможно, префиксы не учитываются, и это положение байта кода операции. Возможно, это имеет значение, или, возможно, макроконфигурированная тестовая ветвь + тянется к следующему блоку?

Макрослияние происходит через границу строки L1I-кеша 64B, так как оно не падает на границу между инструкциями.

Макрослияние не происходит, если первая инструкция заканчивается байтом 63 строки кэша, а вторая команда является условной ветвью, которая начинается с байта 0 следующей строки кэша. - Руководство по оптимизации Intel, 2.3.2.1

Или, может быть, с коротким кодированием для одного прыжка или другого, все по-другому?

Или, может быть, перебор кэша uop не имеет к этому никакого отношения, и это прекрасно, если он быстро декодирует, что происходит в этом выравнивании. Это количество заполнения просто помещает конец UCOMISD в новый блок 16B, поэтому, возможно, это фактически повышает эффективность, позволяя ему декодировать другие инструкции в следующем выровненном блоке 16B. Тем не менее, я не уверен, что необходимо декодировать блок предварительного декодирования 16B (определение длины инструкции) или 32B.


Я также задался вопросом, заканчивает ли CPU переключение из кэша uop в устаревшее декодирование. Это может быть хуже, чем запуск от устаревшего декодирования все время.

Переход от декодеров к кэшу uop или наоборот осуществляется в соответствии с гидом микрогориста Agner Fog. Intel говорит:

Если микрокопы не могут быть сохранены в декодированном ICache из-за этих ограничений, они доставляются из конвейера устаревшего декодирования. Как только микрооперации будут доставлены из унаследованного трубопровода, ops из Decoded ICache могут возобновиться только после следующего микропроцесса ветки. Частые переключатели могут нанести штраф.


Источник, который я собрал + разобрал:

.skip 0x5e
nop
# this is 0x5F
#nop  # OP needed 1B of padding to reach a 32B boundary

.skip 5, 0x90

.globl loop_start
loop_start:
.L37:
  pxor    %xmm0, %xmm0
  cvtsi2sdq   %rcx, %xmm0
  ucomisd %xmm0, %xmm6
  jb  .Loop_exit   // Exit the loop
.L20:
  xorl    %edx, %edx
  movq    %rbx, %rax
  divq    %rcx
  testq   %rdx, %rdx
  je  .Lnot_prime   // Failed divisibility test
  addq    $1, %rcx
  jns .L37

.skip 200  # comment this to make the jumps rel8 instead of rel32
.Lnot_prime:
.Loop_exit:
.skip 0x5e
nop
# this is 0x5F
#nop  # OP needed 1B of padding to reach a 32B boundary

.skip 5, 0x90

.globl loop_start
loop_start:
.L37:
  pxor    %xmm0, %xmm0
  cvtsi2sdq   %rcx, %xmm0
  ucomisd %xmm0, %xmm6
  jb  .Loop_exit   // Exit the loop
.L20:
  xorl    %edx, %edx
  movq    %rbx, %rax
  divq    %rcx
  testq   %rdx, %rdx
  je  .Lnot_prime   // Failed divisibility test
  addq    $1, %rcx
  jns .L37

.skip 200  # comment this to make the jumps rel8 instead of rel32
.Lnot_prime:
.Loop_exit:

Ответ 3

Из того, что я вижу в вашем алгоритме, вы, безусловно, не можете его улучшить.

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

Когда вы пишете две инструкции один за другим, например:

  mov %eax, %ebx
  add 1, %ebx

Чтобы выполнить вторую команду, первая должна быть завершена. По этой причине компиляторы имеют тенденцию смешивать инструкции. Скажем, вам нужно установить %ecx в ноль, вы можете сделать это:

  mov %eax, %ebx
  xor %ecx, %ecx
  add 1, %ebx

В этом случае функции mov и xor могут выполняться параллельно. Это ускоряет работу... Количество команд, которые могут обрабатываться параллельно, очень сильно варьируется между процессорами (Xeons в целом лучше).

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

Наконец, очевидно, что преобразование результата sqrt() в целое число сделает вещи намного быстрее, так как вы избежите всего этого нечувствительного кода SSE2, который будет окончательно медленнее, если использовать только для преобразования + сравнить, когда те две команды могут быть выполнены с целыми числами.

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

Ответ 4

Разницу в производительности можно объяснить различными способами, в которых механизм кодирования команд "видит" инструкции. Процессор читает инструкции в кусках (был на ядре core2 16 байт, я полагаю), и он пытается дать разные суперскалярные микросхемы. Если инструкции находятся на границах или маловероятно, что единицы в одном ядре могут голодать довольно легко.