Ответ 1
Извините, этот ответ получил немного длинный и бессвязный. Я провел несколько тестов, но я долго не редактировал предыдущие материалы, думая о чем-то другом, чтобы попробовать.
Ваш рабочий набор составляет 15.25MiB (16MB). Обычно для сравнения такой процедуры вы будете усреднять меньший буфер несколько раз, поэтому он вписывается в кеш. Вы не видите большой разницы между медленной версией и быстрой версией, поскольку diff скрывается узким местом памяти.
calc_avg1
не автоиндексирует вообще (обратите внимание, что addss
. ss
означает скалярную, одинарную точность, в отличие от addps
(упакованная одиночная точность)). Я думаю, что он не может autovectorize даже когда он встроен в основной, потому что он не может быть уверен, что в 4-й позиции вектора нет NaN
, что вызовет исключение FP, которое не было бы скалярным кодом. Я попытался скомпилировать его для Sandybridge с gcc 4.9.2 -O3 -march=native -ffast-math
и с clang-3.5, но не повезло и с.
Тем не менее, версия, встроенная в main
, работает только медленнее, потому что память является узким местом. 32-битные нагрузки могут почти соответствовать нагрузкам 128b при попадании в основную память. (Однако нестрочная версия будет плохой: каждый результат +=
сохраняется в массиве res
, потому что цикл накапливается непосредственно в памяти, который может иметь другие ссылки на него. Поэтому он должен сделать каждую операцию видимой с помощью магазин. Это версия, которую вы разместили для разборки, для BTW. Сортировка, какая часть основной была использована при компиляции с помощью -S -fverbose-asm
.)
Несколько разочаровывающе, clang и gcc не могут автоматически векторизовать __v4sf
от 4-х до 8-х широкополосного AVX.
Поцарапайте, что после обертывания for (int i=0; i<4000 ; i++)
вокруг вызовов calc_avgX
и уменьшения N
до 10k gcc -O3
превращает внутренний внутренний цикл avg1 в:
400690: c5 f8 10 08 vmovups (%rax),%xmm1
400694: 48 83 c0 20 add $0x20,%rax
400698: c4 e3 75 18 48 f0 01 vinsertf128 $0x1,-0x10(%rax),%ymm1,%ymm1
40069f: c5 fc 58 c1 vaddps %ymm1,%ymm0,%ymm0
4006a3: 48 39 d8 cmp %rbx,%rax
4006a6: 75 e8 jne 400690 <main+0xe0>
$ (get CPU to max-turbo frequency) && time ./a.out
0.016515
1071570752.000000 1066917696.000000 1073897344.000000
0.032875
1071570944.000000 1066916416.000000 1073895680.000000
Это bizzare; Я понятия не имею, почему он не просто использует 32B нагрузки. Он использует 32B vaddps
, что является узким местом при работе с набором данных, который подходит в кэше L2.
IDK почему ему удалось авто-векторизовать внутренний цикл, когда он находился внутри другого цикла. Обратите внимание, что это относится только к версии, встроенной в main
. Вызываемая версия остается только скалярной. Также обратите внимание, что только gcc справился с этим. clang 3.5 не сделал. Может быть, gcc знал, что он будет использовать malloc
таким образом, который возвратил нулевой буфер (так что не нужно было беспокоиться о NaN
в 4-м элементе)?
Я также удивлен, что clang non-vectorized avg1
не медленнее, когда все вписывается в кеш. N=10000
, repeat-count = 40k.
3.3GHz SNB i5 2500k, max turbo = 3.8GHz.
avg1: 0.350422s: clang -O3 -march=native (not vectorized. loop of 6 scalar addss with memory operands)
avg2: 0.320173s: clang -O3 -march=native
avg1: 0.497040s: clang -O3 -march=native -ffast-math (haven't looked at asm to see what happened)
avg1: 0.160374s: gcc -O3 -march=native (256b addps, with 2 128b loads)
avg2: 0.321028s: gcc -O3 -march=native (128b addps with a memory operand)
avg2: ~0.16: clang, unrolled with 2 dependency chains to hide latency (see below).
avg2: ~0.08: unrolled with 4 dep chains
avg2: ~0.04: in theory unrolled-by-4 with 256b AVX. I didn't try unrolling the one gcc auto-vectorized with 256b addps
Таким образом, большой сюрприз заключается в том, что скалярный код clang avg1
поддерживает avg2
. Возможно, цепочка зависимостей, связанная с циклом, является большим узким местом?
perf
показывает 1.47 insns за цикл для clang non-vectorized avg1
, который, вероятно, насыщает блок добавления FP на порт 1. (Большинство команд цикла добавляются).
Однако avg2
, используя 128b addps
с операндом памяти, получает только 0,58 insns за цикл. Уменьшение размера массива на другой коэффициент 10, до N=1000
, получает 0,60 insns за цикл, вероятно, из-за большего количества времени в прологе/эпилоге. Поэтому я думаю, что серьезная проблема с цепочкой зависимостей, связанной с циклом. clang разворачивает цикл на 4, но использует только один аккумулятор. Цикл имеет 7 инструкций, которые декодируют до 10 ударов. (Каждый vaddps
равен 2, так как он используется с операндом памяти с режимом адресации с двумя регистрами, предотвращающим микроплавление. Макро-предохранитель cmp
и jne
). http://www.brendangregg.com/perf.html говорит, что событие perf
для UOPS_DISPATCHED.CORE
равно r2b1
, поэтому:
$ perf stat -d -e cycles,instructions,r2b1 ./a.out
0.031793
1053298112.000000 1052673664.000000 1116960256.000000
Performance counter stats for './a.out':
118,453,541 cycles
71,181,299 instructions # 0.60 insns per cycle
102,025,443 r2b1 # this is uops, but perf doesn't have a nice name for it
40,256,019 L1-dcache-loads
21,254 L1-dcache-load-misses # 0.05% of all L1-dcache hits
9,588 LLC-loads
0 LLC-load-misses:HG # 0.00% of all LL-cache hits
0.032276233 seconds time elapsed
Это подтверждает мои инструкции 7:10 для анализа uops. Это фактически не имеет отношения к проблеме производительности здесь: цикл работает медленнее, чем верхний предел 4 мкп за цикл. Изменение внутреннего цикла для получения двух отдельных цепочек депиляции удваивает пропускную способность (60M циклов вместо 117M, но 81M insns вместо 71M):
for (i=0; i<n-1; i+=2) { // TODO: make sure the loop end condition is correct
r0 += _mm_load_ps (array[i]);
r1 += _mm_load_ps (array[i+1]);
}
r0 += r1;
Развертывание на 4 (с 4 аккумуляторами, которые вы сливаете в конце цикла) снова удваивает производительность. (до 42M циклов, 81M insns, 112M uops.) Внутренний цикл имеет 4x vaddps -0x30(%rcx),%xmm4,%xmm4
(и аналогичный), 2x add
, cmp
, jl
. Эта форма vaddps
должна быть микро-предохранителем, но я все еще вижу намного больше, чем инструкций, поэтому, думаю, r2b1
подсчитывает unused-domain uops. (Linux perf
не имеет хороших документов для специфичных для платформы событий HW). Снова переверните N
, чтобы убедиться, что самая внутренняя петля полностью доминирует во всех подсчетах, я вижу отношение uop: insn 1.39, которое хорошо соответствует 8 insns, 11 uops (1.375) (считая vaddps
как 2, но считая cmp
+ jl
как один). Я нашел http://www.bnikolic.co.uk/blog/hpc-prof-events.html, в котором есть полный список поддерживаемых первичных событий, включая их коды для Sandybridge. (И инструкции о том, как выгрузить таблицу для любого другого процессора). (Ищите строку Code:
в каждом блоке. Вам нужен байт umask, а затем код, как arg для perf
.)
# a.out does only avg2, as an unrolled-by-4 version.
$ perf stat -d -e cycles,instructions,r14a1,r2b1,r10e,r2c2,r1c2 ./a.out
0.011331
1053298752.000000 1052674496.000000 1116959488.000000
Performance counter stats for './a.out':
42,250,312 cycles [34.11%]
56,103,429 instructions # 1.33 insns per cycle
20,864,416 r14a1 # UOPS_DISPATCHED_PORT: 0x14=port2&3 loads
111,943,380 r2b1 # UOPS_DISPATCHED: (2->umask 00 -> this core, any thread).
72,208,772 r10e # UOPS_ISSUED: fused-domain
71,422,907 r2c2 # UOPS_RETIRED: retirement slots used (fused-domain)
111,597,049 r1c2 # UOPS_RETIRED: ALL (unfused-domain)
0 L1-dcache-loads
18,470 L1-dcache-load-misses # 0.00% of all L1-dcache hits
5,717 LLC-loads [66.05%]
0 LLC-load-misses:HG # 0.00% of all LL-cache hits
0.011920301 seconds time elapsed
Так что да, похоже, что это может считать fused-domain и unused-domain uops!
Unrolling by 8 не помогает вообще: все еще 42M циклов. (но до 61M insns и 97M uops, благодаря меньшему количеству накладных расходов на цикл). Аккуратно, clang использует sub $-128, %rsi
вместо добавления, потому что -128 вписывается в imm8
, но +128 этого не делает. Поэтому я предполагаю, что разворачивание на 4 достаточно, чтобы насытить порт добавления FP.
Что касается ваших 1avg-функций, которые возвращают один float, а не вектор, well clang не авто-векторизует первый, но gcc делает. Он испускает гигантский пролог и эпилог для выполнения скалярных сумм, пока не достигнет выровненного адреса, а затем 32B AVX vaddps
в небольшом цикле. Вы говорите, что нашли гораздо большую скорость с ними, но могли ли вы тестировать с меньшим буфером? Это означало бы появление большого ускорения для векторного кода и не-вектора.