L oop занимает менее 1 цикла, несмотря на зависимость между итерациями - PullRequest
1 голос
/ 17 января 2020

Я хотел оценить время, необходимое для одного добавления на моем процессоре 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 оставили меня озадаченным.

1 Ответ

3 голосов
/ 18 января 2020

Частота ядра и частота TS C могут быть разными. Ожидается, что ваш l oop будет работать с 1 базовыми циклами за итерацию. Если частота ядра в два раза превышает частоту TS C на время выполнения l oop, пропускная способность будет 0,5 TS C циклов на итерацию, что эквивалентно 1 циклов ядра за итерацию.

В вашем случае получается, что средняя частота ядра была немного выше, чем частота TS C. Если вы не хотите принимать во внимание динамическое масштабирование частоты c при проведении экспериментов, было бы проще просто зафиксировать частоту ядра, равную частоте TS C, чтобы вам не приходилось преобразовывать числа. В противном случае вам также придется измерять среднюю частоту ядра.

На процессорах, которые поддерживают масштабирование частоты на ядро, вам нужно либо зафиксировать частоту на всех ядрах, либо привязать эксперименты к одному ядро с фиксированной частотой. В качестве альтернативы, вместо измерения в циклах TS C, вы можете использовать инструмент, такой как perf, для простого измерения времени в основных циклах или секундах.

См. Также: Как получить цикл ЦП считать в x86_64 из C ++? .

...