Наиболее очевидной проблемой является использование неэффективного точечного продукта (с haddps
, который стоит 2x случайных мопов + 1x add uop) вместо случайных + add. См. Самый быстрый способ сделать горизонтальную векторную сумму с плавающей запятой на x86 , чтобы узнать, что делать после _mm_mul_ps
, который не так уж и плох. Но все же это не то, что x86 может сделать очень эффективно.
Но, в любом случае, настоящая проблема - ваш тестовый цикл.
A[3] = k;
и затем использование _mm_load_ps(A)
создает стойку пересылки магазина , если она компилируется наивно вместо векторного тасования. Store + reload может быть эффективно перенаправлен с ~ 5 циклами задержки, если загрузка загружает данные только из одной инструкции хранилища, и никаких данных вне этого. В противном случае необходимо выполнить более медленное сканирование всего буфера хранилища, чтобы собрать байты. Это добавляет около 10 циклов задержки к пересылке в магазин.
Я не уверен, какое влияние это окажет на пропускную способность, но этого может быть достаточно, чтобы не допустить, чтобы exec-of-exec перекрывал достаточно итераций цикла, чтобы скрыть задержку и только узкое место при sqrtss
случайной пропускной способности.
(Ваш процессор Coffee Lake имеет пропускную способность 1 на 3 цикла sqrtss
, поэтому на удивление пропускная способность SQRT составляет , а не ваше узкое место. 1 Вместо этого это будет произвольная пропускная способность или что-то еще. )
См. Руководство по микроарху Agner Fog и / или руководство по оптимизации.
Плюс вы еще больше смещаете это по отношению к SSE на , позволяя компилятору поднять вычисление V[0] * V[0] + V[1] * V[1] + V[2] * V[2]
из цикла .
Эта часть выражения является инвариантной к циклу, поэтому компилятору нужно только (float)k
возводить в квадрат, добавлять и скалярный квадрат для каждой итерации цикла. (И преобразовать это в double
, чтобы добавить к вашему аккумулятору).
(удаленный ответ @ StaceyGirl указал на это; просмотр кода внутренних циклов в нем был отличным началом написания этого ответа.)
Дополнительная неэффективность в A [3] = k в векторной версии
Внутренний цикл GCC9.1 из Связь Камиля с Годболтом выглядит ужасно и, кажется, включает в себя перенос / перенос с циклом для объединения нового A[3]
в 8-байтовую пару A[2..3]
, далее ограничение способности процессора перекрывать несколько итераций.
Я не уверен, почему gcc подумал, что это хорошая идея. Это может помочь на процессорах, которые разделяют векторные нагрузки на 8-байтовые половинки (например, Pentium M или Bobcat), чтобы избежать задержек при пересылке из магазина. Но это не нормальная настройка для «универсальных» современных процессоров x86-64.
.L18:
pxor xmm4, xmm4
mov rdx, QWORD PTR [rsp+8] ; reload A[2..3]
cvtsi2ss xmm4, rbx
mov edx, edx ; truncate RDX to 32-bit
movd eax, xmm4 ; float bit-pattern of (float)k
sal rax, 32
or rdx, rax ; merge the float bit-pattern into A[3]
mov QWORD PTR [rsp+8], rdx ; store A[2..3] again
movaps xmm0, XMMWORD PTR [rsp] ; vector load: store-forwarding stall
mulps xmm0, xmm0
haddps xmm0, xmm0
haddps xmm0, xmm0
ucomiss xmm3, xmm0
movaps xmm1, xmm0
sqrtss xmm1, xmm1
ja .L21 ; call sqrtf to set errno if needed; flags set by ucomiss.
.L17:
add rbx, 1
cvtss2sd xmm1, xmm1
addsd xmm2, xmm1 ; total += (double)sqrtf
cmp rbx, 1000000000
jne .L18 ; }while(k<1000000000);
Это безумие отсутствует в скалярной версии.
В любом случае, gcc удалось избежать неэффективности полного преобразования uint64_t
-> float
(которого у x86 не было аппаратно до AVX512). Предположительно удалось доказать, что использование 64-битного преобразования с плавающей запятой со знаком всегда будет работать, поскольку старший бит не может быть установлен.
Сноска 1 : Но sqrtps
имеет такую же пропускную способность 1 на 3 цикла, что и скалярная, так что вы получаете только 1/4 от пропускной способности sqrt вашего ЦП, выполняя по 1 вектору по горизонтали вместо того, чтобы делать 4 длины для 4 векторов параллельно.