Я хотел оценить время, необходимое для одного добавления на моем процессоре Skylake (i5-6500). C достаточно низкий уровень для меня, поэтому я написал следующий код:
// Initializing stuffs
int a = rand();
int b = rand();
const unsigned long loop_count = 1000000000;
unsigned int ignored; // used for __rdtscp
// Warming up whatever needs to be warmed up
for (int i = 0; i < 100000; i++) {
asm volatile("" : "+r" (a)); // prevents Clang from replacing the loop with a multiplication
a += b;
}
// The actual measurement
uint64_t timer = __rdtscp(&ignored);
for (unsigned long i = 0; i < loop_count; i++) {
asm volatile("" : "+r" (a)); // prevents Clang from replacing the loop with a multiplication
a += b;
}
timer = __rdtscp(&ignored) - timer;
printf("%.2f cycles/iteration\n", (double)timer / loop_count);
Компилируя с Clang 7.0.0 -O3, я получаю следующую сборку (только для l oop) :
# %bb.2:
rdtscp
movq %rdx, %rdi
movl %ecx, 4(%rsp)
shlq $32, %rdi
orq %rax, %rdi
movl $1000000000, %eax # imm = 0x3B9ACA00
.p2align 4, 0x90
.LBB0_3: # =>This Inner Loop Header: Depth=1
#APP
#NO_APP
addl %esi, %ebx
addq $-1, %rax
jne .LBB0_3
# %bb.4:
rdtscp
И при выполнении этого кода выдается
0.94 cycles/iteration
(или число, которое почти всегда находится в диапазоне от 0,93 до 0,96)
Я удивлен, что это l oop может выполняться менее чем за 1 цикл / итерацию, поскольку существует зависимость данных от a
, которая должна предотвращать параллельное выполнение a += b
.
IACA
также подтверждает, что ожидаемая пропускная способность составляет 0,96 циклов , llvm-mca
, с другой стороны, предсказывает в общей сложности 104 цикла для выполнения 100 итераций l oop. (При необходимости я могу отредактировать трассировки; дайте мне знать)
Я наблюдаю аналогичное поведение при использовании регистров SSE, а не общих.
Я могу представить, что процессор интеллектуальный достаточно заметить, что b
является константой, и поскольку сложение является коммутативным, оно может развернуть l oop и каким-то образом оптимизировать сложения. Однако я никогда не слышал и не читал об этом ничего. И, кроме того, если бы это было то, что происходило, я бы ожидал лучших результатов (ie. меньше циклов / итерация), чем 0,94 цикла / итерация.
Что происходит? Как этот l oop может выполняться менее чем за 1 цикл за итерацию?
Некоторый фон для полноты. Не обращайте внимания на оставшуюся часть вопроса, если вас не интересует, почему я пытаюсь сопоставить одно добавление.
Я знаю, что существуют инструменты (например, llvm-exegesis), предназначенные для сравнения отдельная инструкция и что я должен вместо них (или просто посмотреть на документы Агнера Тумана). Однако я на самом деле пытаюсь сравнить три разных дополнения : одно делает одно добавление в al oop (объект моего вопроса); один делает 3 добавления на l oop (в регистрах SSE, которые должны максимально использовать порт и не должен быть ограничен зависимостями данных), и один, где дополнение реализовано в виде схемы в программном обеспечении. Пока результаты в основном такие, как я ожидал; 0,94 цикла / итерация для версии с одним добавлением в al oop оставили меня озадаченным.