TL; DR У меня есть все oop, для выполнения которого на Skylake требуется 1 цикл (3 добавления + 1 дюйм / прыжок).
Когда я разверну его больше, чем 2 раза (независимо от того, сколько) моя программа работает примерно на 25% медленнее. Возможно, это как-то связано с выравниванием, но я не совсем понимаю, что.
РЕДАКТИРОВАТЬ: этот вопрос раньше задавался вопросом о том, почему мопы были доставлены DSB, а не MITE. Теперь этот вопрос перенесен на этот вопрос .
Я пытался сравнить al oop, который делает 3 добавления на моем Skylake. Это l oop должно выполняться за один цикл, так как 3 add + 1 приращение сливаются с условным переходом, после того как слияние может выполняться за один цикл. И это, как и ожидалось.
Однако в какой-то момент мой компилятор C попытался развернуть этот l oop, что привело к ухудшению производительности. Сейчас я пытаюсь понять, почему развернутый l oop имеет худшую производительность, чем не развернутый, поскольку я ожидал, что оба будут иметь одинаковую производительность, или, может быть, развернутый будет медленнее, чем 15% .
Вот мой C код:
int main() {
int a, b, c, d;
#pragma unroll(2)
for (unsigned long i = 0; i < 2000000000; i++) {
asm volatile("" : "+r" (a), "+r" (b), "+r" (c), "+r" (d));
a = a + d;
b = b + d;
c = c + d;
}
// Prevent data from being optimized out
asm volatile("" : "+r" (a), "+r" (b), "+r" (c));
}
Компиляция с Clang 7.0.0 -O3 приводит к следующей (очищенной) сборке (с этого момента называемой v1
) :
movl $2000000000, %esi
.p2align 4, 0x90
.LBB0_1:
addl %edi, %edx
addl %edi, %ecx
addl %edi, %eax
addl %edi, %edx
addl %edi, %ecx
addl %edi, %eax
addq $-2, %rsi
jne .LBB0_1
И сравнение с perf stat -e cycles
показывает, что для каждой итерации требуется около 2 циклов.
Однако замена любого из регистров «новым 64-битным регистром» (r8 к r15) заставляет l oop выполняться в 3 циклах вместо 2 (давайте назовем этот код v2
):
movl $2000000000, %esi
.p2align 4, 0x90
.LBB0_1:
addl %edi, %r14d
addl %edi, %ecx
addl %edi, %eax
addl %edi, %r14d
addl %edi, %ecx
addl %edi, %eax
addq $-2, %rsi
jne .LBB0_1
Это не случайный пример: Clang фактически производит это l oop если я добавлю кое-что в свою программу и получу неудачу (моя первоначальная версия была тем же C кодом, с дополнительной случайной инициализацией переменных, фазы разогрева и rdtscp для определения времени l oop и использования Clang r14d
в л oop). Это l oop выполняется примерно за 3 цикла / итерация.
Дальнейшее тестирование показывает, что развертывание l oop любое число раз больше 2 заставляет программу выполняться за 2,5 миллиарда циклов (против 2 миллиардов для не развернутый).
Число мопов в al oop равно 3*n+1
(где n
- коэффициент развертывания, а 1
- слитый add/jne
), что означает, что l oop развернуто 3 раза по 10 мопов; 4 раза 13 мопсов и др. c. Это довольно небольшие количества мопов, которые должны помещаться в DSB (кеш мопов). Я на Skylake с обновленным микрокодом с исправлением для SKL150, так что мой LSD l oop буфер отключен .
Кроме того, развертывание 3, 4, 10 или 50 раз не вообще не меняйте производительность: мой код всегда выполняется за 2,5 миллиарда циклов (тогда как не развернутый код работает за 2 миллиарда циклов). Это несколько удивительно, поскольку 3 сложения должны всегда выполняться в 1 цикле, и, таким образом, если по какой-то причине в конце l oop был потерян дополнительный цикл, его издержки должны амортизироваться при увеличении развертки, а асимптотика * Производительность 1077 * (в коэффициенте развертывания) должна приблизиться к 2 млрд. Циклов.
Оба llvm-mca
и iaca
предсказывают, что при развертывании n раз выполнение l oop будет выполнено в n циклах (что приведет к выполнению всей программы за 2 миллиарда циклов).
Подводя итог, возникает вопрос: почему мои l oop 25% медленнее, как только Я разворачиваюсь более 2 раз?