Скалярное значение в 10 раз вместо 4x медленнее:
Вы получаете ошибки страницы в c[]
внутри скалярной временной области, потому что вы пишете это впервые. Если вы выполняли тесты в другом порядке, какой бы из них ни был первым, вы бы заплатили такой большой штраф. Эта часть является дубликатом этой ошибки: Почему итерация хотя `std :: vector` быстрее, чем итерация хотя `std :: array`? См. также Idiomati c способ оценки производительности?
normal
оплачивает эту стоимость в первом из 5 проходов по массиву , Меньшие массивы и большее число повторов амортизируют это еще больше, но лучше сначала запомнить или иным образом заполнить пункт назначения, чтобы предварительно выполнить поиск неисправности перед временной областью.
normal_res
также скаляр, но пишет в уже загрязненный c[]
. Скалярный в 8 раз медленнее, чем SSE, вместо ожидаемого 4x.
Вы использовали sqrt(double)
вместо sqrtf(float)
или std::sqrt(float)
. На Skylake-X это отлично учитывает дополнительный коэффициент пропускной способности 2 . Посмотрите на вывод asm компилятора в проводнике компилятора Godbolt (G CC 7.4 при условии, что используется та же система, что и ваш последний вопрос ). Я использовал -mavx512f
(что подразумевает -mavx
и -msse
), и не имел опций настройки, чтобы надеяться получить примерно тот же код, что и вы. main
не встроенный normal_res
, поэтому мы можем просто взглянуть на его отдельное определение.
normal_res(float*, float*, float*, unsigned long):
...
vpxord zmm2, zmm2, zmm2 # uh oh, 512-bit instruction reduces turbo clocks for the next several microseconds. Silly compiler
# more recent gcc would just use `vpxor xmm0,xmm0,xmm0`
...
.L5: # main loop
vxorpd xmm0, xmm0, xmm0
vcvtss2sd xmm0, xmm0, DWORD PTR [rdi+rbx*4] # convert to double
vucomisd xmm2, xmm0
vsqrtsd xmm1, xmm1, xmm0 # scalar double sqrt
ja .L16
.L3:
vxorpd xmm0, xmm0, xmm0
vcvtss2sd xmm0, xmm0, DWORD PTR [rsi+rbx*4]
vucomisd xmm2, xmm0
vsqrtsd xmm3, xmm3, xmm0 # scalar double sqrt
ja .L17
.L4:
vaddsd xmm1, xmm1, xmm3 # scalar double add
vxorps xmm4, xmm4, xmm4
vcvtsd2ss xmm4, xmm4, xmm1 # could have just converted in-place without zeroing another destination to avoid a false dependency :/
vmovss DWORD PTR [rdx+rbx*4], xmm4
add rbx, 1
cmp rcx, rbx
jne .L5
vpxord zmm
сокращает турбо тактовые сигналы только на несколько миллисекунд (я думаю) в начале каждого звонка normal
и normal_res
. Он не использует 512-битные операции, поэтому тактовая частота может снова возрасти позже. Отчасти это может быть связано с тем, что оно не является точно 8x.
Сравнение / ja связано с тем, что вы не использовали -fno-math-errno
, поэтому G CC все еще вызывает фактические sqrt
для входных данных <0, чтобы установить <code>errno. Он делает if (!(0 <= tmp)) goto fallback
, прыгает на 0 > tmp
или неупорядочен. «К счастью», sqrt достаточно медленный, чтобы оставаться единственным узким местом. Превышение порядка c преобразования и сравнения / ветвления означает, что блок SQRT по-прежнему остается занятым ~ 100% времени.
vsqrtsd
пропускная способность (6 циклов) в 2 раза медленнее, чем * Пропускная способность 1056 * (3 такта) в Skylake-X, поэтому использование двойной стоимости в скалярной пропускной способности в 2 раза больше.
Scalar sqrt в Skylake-X имеет такую же пропускную способность, что и соответствующие 128-битные PSD / pd SIMD версия. Таким образом, 6 циклов на 1 число в виде double
против 3 циклов на 4 поплавка в виде вектора ps
полностью объясняют коэффициент 8x.
Дополнительное замедление 8x против 10x для normal
было только из-за ошибок страницы.
SSE против AVX sqrt пропускная способность
128-бит sqrtps
достаточно для получения полной пропускной способности SIMD div / кв.м ; если предположить, что это сервер Skylake, как ваш последний вопрос, он имеет ширину 256 бит, но не полностью конвейеризован. Процессор может поочередно отправлять 128-битный вектор в нижнюю или верхнюю половину, чтобы использовать преимущества полной аппаратной ширины, даже если вы используете только 128-битные векторы. См. Деление с плавающей запятой и умножение с плавающей запятой (FP div и sqrt выполняются на одном и том же модуле выполнения.)
См. Также значения задержки / пропускной способности для команд https://uops.info/ или https://agner.org/optimize/.
Все add / sub / mul / fma имеют ширину 512 бит и полностью конвейеризованы; используйте это (например, для оценки полинома 6-го порядка или чего-то еще), если вы хотите что-то, что может масштабироваться с векторной шириной. div / sqrt является особым случаем.
Вы ожидаете выгоды от использования 256 -битовые векторы для SQRT, только если у вас было узкое место во внешнем интерфейсе (4 / тактовая инструкция / пропускная способность uop), или если вы выполняли кучу операций add / sub / mul / fma с векторами.
256-бит не хуже , но это не помогает, когда единственное узкое место в вычислениях - пропускная способность блока div / sqrt.
См. Статью Джона Маккальпина Ответьте для более подробной информации о стоимости только для записи примерно так же, как чтение + запись из-за RFO
С таким небольшим количеством вычислений на доступ к памяти, вы, вероятно, близки к узким местам в пропускной способности памяти снова / снова. Даже если аппаратное обеспечение FP SQRT было шире / быстрее, на практике ваш код может не работать быстрее. Вместо этого ядро будет тратить больше времени, ничего не делая, ожидая поступления данных из памяти.
Кажется, вы получаете именно ожидаемое ускорение от 128-битных векторов (2x * 4x = 8x), так что, очевидно, версия __m128 не является узким местом и для пропускной способности памяти.
2x sqrt на 4 обращения к памяти примерно такой же, как a[i] = sqrt(a[i])
(1x sqrt на загрузку + хранилище), который вы делали в опубликованном вами коде в чате , но вы не дали никаких цифр для этого. Это позволило избежать проблемы с ошибкой страницы, поскольку после инициализации он переписывал массив на месте.
В общем случае переписывание массива на месте - хорошая идея, если вы по какой-то причине продолжаете настаивать на пытаясь получить ускорение SIMD 4x / 8x / 16x, используя эти безумно огромные массивы, которые даже не помещаются в кэш-память третьего уровня. (при условии последовательного доступа, чтобы предварительные сборщики могли извлекать его непрерывно, не вычисляя следующий адрес): более быстрое вычисление не ускоряет общий прогресс. Строки кэша поступают из памяти с некоторой фиксированной максимальной пропускной способностью, причем ~ 12 строк кэша передаются за один раз (12 LFB в Skylake). Или «superqueue» L2 может отслеживать больше строк кэша, чем это (может быть, 16?), Поэтому предварительная выборка L2 будет считываться раньше, чем остановится ядро ЦП.
Пока ваши вычисления могут идти в ногу с эта скорость, делая ее быстрее, просто оставит больше циклов бездействия, прежде чем появится следующая строка кэша.
(Буфер хранилища, записывающий обратно в L1d и затем удаляющий грязные строки, также происходит, но основной c идея о том, что ядро ожидает память, все еще работает.)
Вы можете думать об этом, как об остановке и go traffi c в машине : a щель открывается впереди твоей машины. Сокращение этого разрыва быстрее не даст вам никакой средней скорости, это просто означает, что вы должны остановиться быстрее.
Если вы хотите увидеть преимущества AVX и AVX512 над SSE, вам потребуется меньше массивы (и более высокий счетчик повторений). Или вам потребуется много работы ALU для каждого вектора, например, полином.
Во многих реальных задачах одни и те же данные используются многократно, поэтому кеши работают. И можно разбить вашу задачу на выполнение нескольких операций с одним блоком данных, пока он горячий в кэше (или даже при загрузке в регистрах), чтобы увеличить вычислительную интенсивность, достаточную для того, чтобы воспользоваться преимуществами баланса вычислений и памяти современных процессоров. .