Оба скомпилированы с 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
задержка), внешний интерфейс легко будет опережать внутренний, даже если ему придется конкурировать с другой гиперпотокой.