mihaildДействительно, спасибо, почему-то я помнил что блок FMA один, а не два, видимо из-за скалярных умножений MUL/IMUL/MULX, в основном ими пользуюсь, на ширину AVX домножил, а про второй блок не вспомнил, и в pdf поленился глянуть.
Но! В процессоре лишь один порт записи результатов в память, следовательно можно выполнить не более 4 double итераций за такт (сохранить лишь один регистр). И лишь два порта чтения памяти, потому прочитать можно не более двух регистров за такт, например a[] и b[], что тоже ограничивает до 4 double итераций на такт. Потому коэффициент таки 4 вместо 8. И выигрыша от применения FMA вместо AVX нет.
Далее все цифры уже строго по pdf на Skylake от Агнера Фога.
если вычисления идут на 1 ядре и без всяких там AVX2 команд FMA, а по старинке - то приведённую Вами цифру нужно делить на 4х4=16, т.е. получится что то около 3,75 млрд. оп./ сек, что довольно близко к наблюдаемому?
Если по старинке, т.е. на FPU, то FMUL занимает 1 такт в потоке и выполняется в 0 порту, команда FSUB выполняется за 1 такт в потоке в 5 порту, две команды FLD выполняются параллельно в любом из 2 и 3 портов за 1 такт в потоке, плюс команда FSTP в 4+7 портах за 1 такт в потоке. Итого получается 1 операция обязательно в 0 порту, одна операция обязательно в 5 порту, две операции в любом из 2 или 3 портов и одна операция в 4+7 портах, в идеале должно уложиться в 1 такт во всех задействованных портах, т.е. 1 такт на итерацию. Надеюсь
Вы/компилятор будете держать в FPU регистре. При частоте 3.4-4 ГГц это составит 3.4-4 млрд итераций в секунду (на ядро), что вдвое выше измеренного Вами.
С другой стороны, вместо FPU компиляторы давно перешли на использование xmm регистров даже для скалярных вычислений, посчитаем для них скорость: MULSD и SUBSD выполняются в любом из 0 или 1 портах и занимают 1 такт в потоке, MOVSD в обе стороны выполняются в других портах (2/3 и 4+7) и занимают тот же 1 такт в потоке. Итого все нужные операции могут выполниться за 1 такт (в портах 0 (MULSD), 1 (SUBSD), 2 (MOVSD из памяти), 3 (MULSD из памяти), 4+7 (MULSD в память), всего 5 операций за такт при максимум 6-ти допустимых) и дать скорость равную частоте процессора в 3.4-4 млрд итераций в секунду (на ядро).
Дополнительно оценим влияние команд организации цикла: DEC/SUB плюс JNZ спарятся и могут выполниться в свободном от вычислений 6-м порту за 1 такт, что на скорость не повлияет никак.
Раз у Вас скорость вдвое меньше, значит в поток вмешивается ещё какая-то лишняя в идеале команда, например загрузка в регистр величины
или счётчика цикла. Или PREFETCH в каждом цикле. Или, скорее, и то и другое, т.к. по отдельности они займут лишь по полтакта в потоке и снизят скорость в полтора раза вместо двух. Хотя если счётчик цикла хранится в памяти (что идиотизм), то снижение скорости будет как раз ровно в два раза. Или Вы немного ошиблись с измерением скорости и она порядка 2.3 млрд итераций в секунду на частоте 3.4 ГГц (хотя у меня под даже ещё большей нагрузкой частота повышается на два шага, у Вас должна до 3.6-3.7 ГГц и 2.4 млрд итераций в секунду). На ядро разумеется.
Это всё было для скалярных вычислений, без векторизации цикла.
С векторизацией (руками или компилятором) скорость должна быть порядка 15 млрд итераций в секунду на ядро (частоту умножить на 1 операцию MOVDQA в память за такт и на 4 итерации в регистре для double). В памяти производится три операции (два чтения и одна запись) на 4 итерации, потому пропускная способность памяти тоже может ограничивать скорость, она должна быть не менее 24х скорости итераций (для double). Для распространённой памяти DDR4 2133МГц в двухканальном режиме полоса пропускания составляет 34 ГБ/с, что эквивалентно всего лишь 1.4 млрд double итераций в секунду (векторно!). Это применимо для объёмов данных примерно втрое и более больше объёма кэша L3, меньшие объёмы будут эффективно кэшироваться.
По идее указанные скорости не должны зависеть от размещения массивов в памяти или кэше данных любого уровня (за одним исключением для векторизированного цикла, см. выше) так как процессор аппаратно поддерживает предвыборку двух массивов из памяти и к моменту запроса данных (для массивов достаточной длины, более сотен элементов, чтобы латентность памяти незначительно повлияла на время вычислений) они уже окажутся в кэше L1. Для
гарантии этого можно в код добавить команду PREFETCH, которая хоть и займёт любой из портов 2 или 3, но её можно подавать лишь дважды (для двух массивов) для 64 байтов линии кэша L1 (или 8 итераций), т.е.
в идеале она замедлит выполнение лишь на
(такты∙команды/итераций/портов), что явно несущественно.
-- 10.03.2020, 20:10 -- т.е. получится что то около 3,75 млрд. оп./ сек, что довольно близко к наблюдаемому?
Простите, не удержался, это не "довольно близко", это
аж вдвое! "Довольно близко" это процентов 10-20, ну пусть даже 25, столько я обычно готов списать на точность измерений. Если у Вас точность измерений составляет
разы, то ... я зря распинаюсь про детали вычислений в процессоре, уж "сравнимо в разы" будет выполняться почти любой код. Мне не жалко, но смысл теряется. Ещё раз простите.