Рассмотрим эту простую функцию C++ для вычисления суммы префикса массива:
void prefix_sum(const uint32_t* input, uint32_t* output, size_t size) {
uint32_t total = 0;
for (size_t i = 0; i < size; i++) {
total += input[i];
output[i] = total;
}
}
Цикл компилируется в следующую сборку в gcc 5.5:
.L5:
add ecx, DWORD PTR [rdi+rax*4]
mov DWORD PTR [rsi+rax*4], ecx
add rax, 1
cmp rdx, rax
jne .L5
Я не вижу ничего, что могло бы помешать этому запускаться с 1 циклом на итерацию, но я последовательно измеряю его на 1,32 (+ / - 0,01) циклов/итерация на моем Skylake i7-6700HQ при работе с 8 КиБ массивы ввода/вывода.
Цикл подается из кэша UOP и не пересекает границы кэша UOP, а счетчики производительности не указывают на узкое место внешнего интерфейса.
Это 4 слитка мопс 1, и этот процессор может выдержать 4 слияния операций/цикл.
Через ecx
и rax
проходят цепочки зависимостей, каждый из которых состоит из 1 цикла, но эти мопы add
могут идти на любой из 4 портов ALU, поэтому вряд ли конфликтуют. Слитый cmp
должен перейти к p6, что вызывает больше беспокойства, но я измеряю только 1.1 моп/итерацию до p6. Это объясняет 1,1 цикла на одну итерацию, но не 1,4. Если я разверну цикл в 2 раза, давление порта будет намного ниже: менее 0,7 мопа для всего p0156, но производительность все равно будет неожиданно низкой - 1,3 цикла за итерацию.
На одну итерацию приходится один магазин, но мы можем сделать один магазин за цикл.
На одну итерацию приходится одна загрузка, но мы можем выполнить две из них за цикл.
На цикл приходится два сложных AGU, но мы можем сделать два из них за цикл.
Что за узкое место здесь?
Интересно, что я попробовал предиктор производительности Itermal, и он почти полностью соответствует действительности: оценка 1.314 циклов против моего измерения 1.32.
1 Я подтвердил слияние макро- и микросинтеза с помощью счетчика uops_issued.any
, который считает в слитой области и считывает 4,0 слитых мопов за одну итерацию для этой циклы.