Почему время выполнения процесса короче, когда другой процесс использует то же ядро ​​HT - PullRequest
4 голосов
/ 25 сентября 2019

У меня есть процессор Intel с 4 ядрами HT (8 логических процессоров), и я построил два простых процесса.

Первый:

int main()
{
  for(int i=0;i<1000000;++i)
    for(int j=0;j<100000;++j);
}

Второй:

int main()
{
  while(1);
}

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

Когда я запускаю первый на первом логическом CPU (CPU0), а когда другой логическийЗагрузка процессоров около 0%, время выполнения этого первого процесса:

real    2m42,625s
user    2m42,485s
sys     0m0,070s

Однако, когда я запускаю второй процесс (бесконечный цикл) на CPU4 (CPU0 и CPU4 находятся на одном и том жеядро, но не в одном и том же аппаратном потоке), время выполнения первого процесса составляет

real    2m25,412s
user    2m25,291s
sys     0m0,047s

Я ожидал более продолжительного времени, поскольку на одном ядре два процесса, а не только один.Но это на самом деле быстрее.Почему это происходит?

РЕДАКТИРОВАТЬ: драйвер P-состояний является intel_pstate.C-состояния фиксируются с помощью processor.max_cstate=1 intel_idle.max_cstate=0.Регулятор частоты настроен на производительность (cpupower frequency-set -g performance), а турбо отключено (cat /sys/devices/system/cpu/intel_pstate/no_turbo дает 1)

1 Ответ

4 голосов
/ 29 сентября 2019

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

В отличие от обычной программы, версия с циклом int i,j полностью устраняет узкие места в магазине.задержка переадресации, а не ресурсы внешней пропускной способности или выполнения или какие-либо общие ресурсы.

Именно поэтому вы никогда не захотите проводить настоящий бенчмаркинг с -O0 режимом отладки: узкие места отличаются по сравнению с обычной оптимизацией (по крайней мере -O2, предпочтительно -O3 -march=native).


В семействе Intel Sandybridge (включая ЦП @abyven_mark's Kaby Lake), задержка пересылки магазина ниже , если повторная загрузка не пытается запустить сразу после хранилища, а вместо этого запускает пару циклов позже. Добавление избыточного назначения ускоряет код при компиляции без оптимизации а также Цикл с вызовом функции быстрее, чем пустой цикл оба демонстрируют этот эффект в неоптимизированных выходных данных компилятора.

Наличие еще одного гиперпотока, конкурирующего за полосу пропускания внешнего интерфейса, по-видимому, иногда приводит к этому.

Или, может быть, статическое разбиение буфера хранилища ускоряет пересылку хранилища? Может быть интереснопопробовать минимально-инвазивный цикл, работающий на другом ядре, например:

// compile this with optimization enabled
// and run it on the HT sibling of the debug-mode nested loop
#include  <immintrin.h>

int main(void) {
    while(1) {
      _mm_pause(); _mm_pause();
      _mm_pause(); _mm_pause();
    }
}

pause блоков в течение примерно 100 циклов на Skylake, по сравнению с 5 на более ранних процессорах.

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

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

Обновление: @uneven_mark протестировано на Kaby Lake и обнаружило, что это уменьшило "ускорение" до ~ 2%, по сравнению с ~ 8%.Таким образом, очевидно, что конкуренция за внешние / внутренние ресурсы была важной частью бесконечного цикла, мешавшего другому циклу слишком быстро перезагружаться.

Возможно, использование слотов BOB (branch-order-buffer) былоосновной механизм, препятствующий выдаче мопов ветвями другого потока в нерабочий сервер.Современные процессоры x86 делают снимок RAT и другого состояния бэкэнда, чтобы обеспечить быстрое восстановление при обнаружении ошибочных прогнозов ветвления, позволяя выполнить откат к неверно предсказанному ветвлению, не дожидаясь его выхода на пенсию.

Это позволяет избежать ожидания независимой работы до ветвь, и продолжение ее восстановления после восстановления продолжается.Но это означает, что в полете может быть меньше веток.Хотя бы меньше условных / косвенных веток?IDK, если прямой jmp будет использовать запись BOB;срок его действия устанавливается при декодировании.Поэтому, возможно, это предположение не содержит воды.


В цикле while(1){} нет локальных переменных в цикле, поэтому он не является узким местом при пересылке в магазин.Это просто цикл top: jmp top, который может выполняться по 1 циклу за итерацию.Это инструкция для одного мопа для Intel.

i5-8250U - это Kaby Lake , и (в отличие от Coffee Lake) все еще имеет свой буфер буфера (LSD), отключенный микрокодом, подобным Skylake.Таким образом, он не может развернуть себя в LSD / IDQ (очередь, заполняющая этап выпуска / переименования) и должен извлекать jmp uop отдельно от кэша uop каждый цикл.Но IDQ буферизует это, требуя только цикла выпуска / переименования каждые 4 цикла, чтобы выпустить группу из 4 jmp uops для этого логического ядра.

Но, в любом случае, в SKL / KBL эти два потока вместе больше, чем насыщают пропускную способность кэша UOP, и конкурируют друг с другом таким образом .На процессоре с включенным LSD (буфером обратной связи) (например, Haswell / Broadwell или Coffee Lake и выше) они не будут.Sandybridge / Ivybridge не разворачивают крошечные петли, чтобы использовать больше их LSD, чтобы у вас был такой же эффект.Я не уверен, что это важно. Было бы интересно проверить на Haswell или Coffee Lake.

(Безусловное jmp всегда заканчивает строку UOP-кэша, и в любом случае это не кэш трассировки, поэтому одна выборка UOP-кэша можетне дает вам более одного jmp моп.)


Я должен исправить свое подтверждение сверху: я скомпилировал все программы как C ++ (g ++), что дало примерно 2%разница.Если я скомпилирую все как C, я получу около 8%, что ближе к OP примерно на 10%.

Интересно, gcc -O0 и g++ -O0 делают компиляцию циклов по-разному.Это извращение внешних интерфейсов GCC C и C ++, обеспечивающих различные GIMPLE / RTL серверного GCC, или что-то в этом роде, и -O0, не позволяющих фонову исправлять неэффективность. Это не является чем-то фундаментальным в C против C ++ или в том, что вы могли ожидать от других компиляторов.

Версия C по-прежнему преобразуется в идиоматический цикл в стиле do{}while() с cmp/jle внижняя часть цикла, справа после добавления в память назначения.(Левая панель этой ссылки проводника компилятора Годболта ). Почему циклы всегда компилируются в стиле "do ... while" (прыжок в хвост)?

Но в версии C ++ используется стиль циклов if(break) с условием вверху,затем память-назначение добавить. Забавно, что отделение памяти-назначения add от перезагрузки cmp только одной инструкцией jmp имеет такое большое значение.

# inner loop, gcc9.2 -O0.   (Actually g++ -xc but same difference)
        jmp     .L3
.L4:                                       # do {
        add     DWORD PTR [rbp-8], 1       #   j++
.L3:                                  # loop entry point for first iteration
        cmp     DWORD PTR [rbp-8], 99999
        jle     .L4                        # }while(j<=99999)

Очевидно, что add / cmp backчтобы эта версия больше страдала от медленной пересылки магазина на Skylake / Kaby / Coffee Lake

против.этот, который не затронут так сильно:

# inner loop, g++9.2 -O0
.L4:                                      # do {
        cmp     DWORD PTR [rbp-8], 99999
        jg      .L3                         # if(j>99999) break
        add     DWORD PTR [rbp-8], 1        # j++
        jmp     .L4                       # while(1)
.L3:

cmp [mem], imm / jcc может все еще быть микро- и / или макро-предохранителем, но я забыл, какой.IDK, если это уместно, но если цикл больше мопов, он не может работать так быстро.Тем не менее, с узким местом выполнения в 1 итерацию на 5 или 6 циклов (задержка памяти add задержка), внешний интерфейс легко будет опережать внутренний, даже если ему придется конкурировать с другой гиперпотокой.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...