Почему Мулсс занимает всего 3 цикла в Haswell, в отличие от таблиц инструкций Агнера?
Я новичок в оптимизации инструкций.
Я сделал простой анализ простой функции dotp, которая используется для получения точечного произведения двух массивов с плавающей точкой.
Код C выглядит следующим образом:
float dotp(
const float x[],
const float y[],
const short n
)
{
short i;
float suma;
suma = 0.0f;
for(i=0; i<n; i++)
{
suma += x[i] * y[i];
}
return suma;
}
Я использую тестовый фрейм, предоставленный Агнером Фогом на веб- тесте.
Массивы, которые используются в этом случае, выровнены:
int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);
float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;
Затем я вызываю функцию dotp, n=2048, repeat=100000:
for (i = 0; i < repeat; i++)
{
sum = dotp(x,y,n);
}
Я компилирую его с помощью gcc 4.8.3, с опцией компиляции -O3.
Я скомпилировал это приложение на компьютере, который не поддерживает инструкции FMA, поэтому вы можете видеть, что есть только инструкции SSE.
Код сборки:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
Я делаю некоторый анализ:
μops-fused la 0 1 2 3 4 5 6 7
movss 1 3 0.5 0.5
mulss 1 5 0.5 0.5 0.5 0.5
add 1 1 0.25 0.25 0.25 0.25
cmp 1 1 0.25 0.25 0.25 0.25
addss 1 3 1
jg 1 1 1 -----------------------------------------------------------------------------
total 6 5 1 2 1 1 0.5 1.5
После запуска получаем результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
--------------------------------------------------------------------
542177906 |609942404 |1230100389 |205000027 |261069369 |205511063
--------------------------------------------------------------------
2.64 | 2.97 | 6.00 | 1 | 1.27 | 1.00
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-----------------------------------------------------------------------
205185258 | 205188997 | 100833 | 245370353 | 313581694 | 844
-----------------------------------------------------------------------
1.00 | 1.00 | 0.00 | 1.19 | 1.52 | 0.00
Вторая строка - это значение, считываемое из регистров Intel; третья строка делится на номер ветви "BrTaken".
Итак, мы видим, что в цикле есть 6 инструкций, 7 мопов, в соответствии с анализом.
Число мопов, запущенных на порт 0, порт 1, порт 5, порт 6, аналогично тому, что говорится в анализе. Я думаю, может быть, планировщик Uops делает это, он может попытаться сбалансировать нагрузки на порты, я прав?
Я абсолютно не понимаю, знаю, почему существует только около 3 циклов на цикл. Согласно таблице инструкций Агнера, задержка обучения mulss
равно 5, и между циклами есть зависимости, так что, насколько я вижу, должно быть не менее 5 циклов на цикл.
Может ли кто-нибудь пролить некоторое понимание?
================================================== ================
Я попытался написать оптимизированную версию этой функции в nasm, развернув цикл в 8 раз и используя vfmadd231ps
инструкция:
.L2:
vmovaps ymm1, [rdi+rax]
vfmadd231ps ymm0, ymm1, [rsi+rax]
vmovaps ymm2, [rdi+rax+32]
vfmadd231ps ymm3, ymm2, [rsi+rax+32]
vmovaps ymm4, [rdi+rax+64]
vfmadd231ps ymm5, ymm4, [rsi+rax+64]
vmovaps ymm6, [rdi+rax+96]
vfmadd231ps ymm7, ymm6, [rsi+rax+96]
vmovaps ymm8, [rdi+rax+128]
vfmadd231ps ymm9, ymm8, [rsi+rax+128]
vmovaps ymm10, [rdi+rax+160]
vfmadd231ps ymm11, ymm10, [rsi+rax+160]
vmovaps ymm12, [rdi+rax+192]
vfmadd231ps ymm13, ymm12, [rsi+rax+192]
vmovaps ymm14, [rdi+rax+224]
vfmadd231ps ymm15, ymm14, [rsi+rax+224]
add rax, 256
jne .L2
Результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
24371315 | 27477805| 59400061 | 3200001 | 14679543 | 11011601
------------------------------------------------------------------------
7.62 | 8.59 | 18.56 | 1 | 4.59 | 3.44
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-------------------------------------------------------------------------
25960380 |26000252 | 47 | 537 | 3301043 | 10
------------------------------------------------------------------------------
8.11 |8.13 | 0.00 | 0.00 | 1.03 | 0.00
Таким образом, мы можем видеть, что кэш данных L1 достигает 2*256 бит /8,59, он очень близок к пику 2*256/8, использование составляет около 93%, блок FMA используется только 8/8,59, пик составляет 2*8/8, использование составляет 47%.
Так что я думаю, что я достиг узкого места L1D, как ожидает Питер Кордес.
================================================== ================
Отдельное спасибо Boann, исправьте так много грамматических ошибок в моем вопросе.
================================================== ===============
Из ответа Питера я понял, что только регистр "чтения и записи" будет зависимостью, регистры "только для записи" не будут зависимостью.
Поэтому я пытаюсь уменьшить количество регистров, используемых в цикле, и пытаюсь развернуть их на 5, если все в порядке, я должен встретить то же самое узкое место, L1D.
.L2:
vmovaps ymm0, [rdi+rax]
vfmadd231ps ymm1, ymm0, [rsi+rax]
vmovaps ymm0, [rdi+rax+32]
vfmadd231ps ymm2, ymm0, [rsi+rax+32]
vmovaps ymm0, [rdi+rax+64]
vfmadd231ps ymm3, ymm0, [rsi+rax+64]
vmovaps ymm0, [rdi+rax+96]
vfmadd231ps ymm4, ymm0, [rsi+rax+96]
vmovaps ymm0, [rdi+rax+128]
vfmadd231ps ymm5, ymm0, [rsi+rax+128]
add rax, 160 ;n = n+32
jne .L2
Результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
25332590 | 28547345 | 63700051 | 5100001 | 14951738 | 10549694
------------------------------------------------------------------------
4.97 | 5.60 | 12.49 | 1 | 2.93 | 2.07
uop p2 |uop p3 | uop p4 | uop p5 |uop p6 | uop p7
------------------------------------------------------------------------------
25900132 |25900132 | 50 | 683 | 5400909 | 9
-------------------------------------------------------------------------------
5.08 |5.08 | 0.00 | 0.00 |1.06 | 0.00
Мы видим 5/5,60 = 89,45%, это немного меньше, чем уроллинг на 8, что-то не так?
================================================== ===============
Я пытаюсь развернуть цикл на 6, 7 и 15, чтобы увидеть результат. Я также снова разверну на 5 и 8, чтобы дважды подтвердить результат.
Результат следующий: мы видим, что на этот раз результат намного лучше, чем раньше.
Хотя результат нестабилен, коэффициент развертывания больше, а результат лучше.
| L1D bandwidth | CodeMiss | L1D Miss | L2 Miss
----------------------------------------------------------------------------
unroll5 | 91.86% ~ 91.94% | 3~33 | 272~888 | 17~223
--------------------------------------------------------------------------
unroll6 | 92.93% ~ 93.00% | 4~30 | 481~1432 | 26~213
--------------------------------------------------------------------------
unroll7 | 92.29% ~ 92.65% | 5~28 | 336~1736 | 14~257
--------------------------------------------------------------------------
unroll8 | 95.10% ~ 97.68% | 4~23 | 363~780 | 42~132
--------------------------------------------------------------------------
unroll15 | 97.95% ~ 98.16% | 5~28 | 651~1295 | 29~68
================================================== ===================
Я пытаюсь скомпилировать функцию с gcc 7.1 в сети " https://gcc.godbolt.org/"
Опция компиляции - "-O3 -march=haswell -mtune=intel", аналогичная gcc 4.8.3.
.L3:
vmovss xmm1, DWORD PTR [rdi+rax]
vfmadd231ss xmm0, xmm1, DWORD PTR [rsi+rax]
add rax, 4
cmp rdx, rax
jne .L3
ret
1 ответ
Посмотрите на вашу петлю еще раз: movss xmm1, src
не зависит от старого значения xmm1
потому что его назначение только для записи. Каждая итерация mulss
является независимым Внеочередное выполнение может использовать и использует этот параллелизм на уровне команд, так что вы определенно не ограничиваетесь mulss
задержка.
Необязательное чтение: в терминах архитектуры компьютера: переименование регистра позволяет избежать опасности, связанной с данными WAR, от повторного использования одного и того же архитектурного регистра. (Некоторые схемы конвейерной обработки + отслеживания зависимостей до переименования регистров не решили всех проблем, поэтому в области компьютерной архитектуры существует множество угроз для данных.
Переименование регистров с помощью алгоритма Томасуло убирает все, кроме фактических истинных зависимостей (чтение после записи), поэтому любая инструкция, в которой пункт назначения не является также регистром-источником, не имеет взаимодействия с цепочкой зависимостей, включающей старое значение этого регистра. (За исключением ложных зависимостей, таких как popcnt
на процессорах Intel, и запись только части регистра без очистки остальных (как mov al, 5
или же sqrtss xmm2, xmm1
). Связано: почему большинство команд x64 обнуляют верхнюю часть 32-битного регистра).
Вернуться к вашему коду:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
Зависимости, переносимые циклом (от одной итерации к следующей), каждая:
xmm0
читать и писатьaddss xmm0, xmm1
, который имеет 3 цикла задержки на Haswell.rax
читать и писатьadd rax, 1
, 1с задержка, так что это не критический путь.
Похоже, вы правильно измерили время выполнения / количество циклов, потому что узкие места цикла в 3c addss
задержка.
Это ожидаемо: последовательная зависимость в точечном произведении - это сложение в единую сумму (или сокращение), а не умножение между векторными элементами.
Это, безусловно, главное узкое место для этого цикла, несмотря на различные незначительные недостатки:
short i
произвел глупость cmp cx, ax
, который принимает дополнительный префикс размера операнда. К счастью, GCC удалось избежать на самом деле add ax, 1
Поскольку переполнение со знаком является неопределенным поведением в C. Таким образом, оптимизатор может предположить, что этого не происходит. (обновление: целочисленные правила продвижения делают его другим для short
, поэтому UB не входит в это, но gcc все еще может юридически оптимизировать. Довольно дурацкий материал.)
Если бы вы скомпилировали с -mtune=intel
, или лучше, -march=haswell
GCC бы положил cmp
а также jg
рядом друг с другом, где они могли бы слиться в макро.
Я не уверен, почему у вас есть *
в вашем столе на cmp
а также add
инструкции. (обновление: я просто догадывался, что вы использовали нотацию, как это делает IACA, но, видимо, это не так). Ни один из них не перегорел. Единственное слияние происходит микро-слияние mulss xmm1, [rsi+rax*4]
,
А поскольку это инструкция ALU с двумя операндами с регистром назначения чтения-изменения-записи, она остается слитой в макросе даже в ROB на Haswell. (Sandybridge разложил бы его во время выпуска.) Обратите внимание, что vmulss xmm1, xmm1, [rsi+rax*4]
разложил бы ламинат на Haswell, также.
Ничто из этого на самом деле не имеет значения, поскольку вы просто полностью ограничиваете задержку добавления FP, намного медленнее, чем любые ограничения пропускной способности. Без -ffast-math
, компиляторы ничего не могут сделать. С -ffast-math
, clang обычно разворачивается с несколькими аккумуляторами и автоматически векторизуется, поэтому они будут векторными аккумуляторами. Таким образом, вы можете, вероятно, насытить предел пропускной способности Haswell в 1 векторное или скалярное прибавление FP за такт, если попадете в кэш L1D.
С FMA с задержкой 5c и пропускной способностью 0.5c на Haswell вам потребуется 10 аккумуляторов, чтобы поддерживать 10 FMA в полете и максимально увеличивать пропускную способность FMA, поддерживая насыщение p0/p 1 FMA. (Skylake уменьшил задержку FMA до 4 циклов и выполняет умножение, сложение и FMA на устройствах FMA. Таким образом, у него на самом деле задержка при добавлении выше, чем у Haswell.)
(Вы ограничены в нагрузках, потому что вам нужно две загрузки для каждого FMA. В других случаях вы можете увеличить пропускную способность, заменив некоторые vaddps
инструкция с FMA с множителем 1,0. Это означает большую задержку для сокрытия, поэтому лучше использовать более сложный алгоритм, в котором у вас есть дополнение, которое не находится на критическом пути.)
Re: моп на порт:
в порту 5 имеется 1,19 мопов на цикл, это намного больше, чем ожидалось 0,5, это вопрос о том, что диспетчер мопов пытается сделать мопы на каждом порту одинаковыми
Да что-то подобное.
Мопы не назначаются случайным образом или как-то равномерно распределены по каждому порту, на котором они могут работать. Вы предполагали, что add
а также cmp
мопс будет равномерно распределяться по p0156, но это не так.
Этап выпуска назначает мопы на порты в зависимости от того, сколько мопов уже ожидает этот порт. поскольку addss
может работать только на p 1 (и это узкое место в цикле), обычно много p 1-мопов выдается, но не выполняется. Поэтому немногие другие мопы когда-либо будут запланированы на port1. (Это включает mulss
: большинство из mulss
мопс в конечном итоге запланирован на порт 0.)
Taken-ветки могут работать только на порту 6. Порт 5 не имеет никаких мопов в этом цикле, которые могут работать только там, поэтому он в конечном итоге привлекает много мопов с многими портами.
Планировщик (который выбирает мопы неиспользованного домена из станции резервирования) не достаточно умен, чтобы запускать критический путь в первую очередь, поэтому этот алгоритм назначения уменьшает задержку конфликта ресурсов (другие мопы крадут порт 1 в циклах, когда addss
мог бы бежать). Это также полезно в тех случаях, когда вы ограничиваете пропускную способность данного порта.
Планирование уже назначенных мопов, как я понимаю, обычно выполняется в первую очередь. Этот простой алгоритм вряд ли удивителен, так как он должен выбрать uop со своими входами, готовыми для каждого порта, из 60-входного RS каждый такт, без потери производительности вашего процессора. Неисправный механизм, который находит и использует ILP, является одной из значительных затрат на электроэнергию в современном ЦП, сравнимой с исполнительными блоками, которые выполняют реальную работу.
Связанные / более подробная информация: Как точно запланировано выполнение x86-мопов?
Больше материала для анализа производительности:
Помимо пропусков кэша / неверных предсказаний ветвления, существуют три основных возможных узких места для циклов, связанных с процессором:
- цепочки зависимостей (как в этом случае)
- внешняя пропускная способность (максимум 4 Uops из слитого домена выдается за такт на Haswell)
- узкие места порта выполнения, например, если многим мопам нужен p0/p 1 или p2/p3, как в вашем развернутом цикле. Подсчитайте число мопов в домене без использования для определенных портов. Как правило, вы можете предполагать распределение в лучшем случае, когда мопы могут запускаться на других портах, не украдя занятые порты очень часто, но это действительно случается.
Тело цикла или короткий блок кода можно приблизительно охарактеризовать с помощью 3-х вещей: счетчик числа операций с плавкой областью, счетчик неиспользуемой области, на каких исполнительных блоках он может работать, и общая задержка критического пути, предполагая планирование в лучшем случае для своего критического пути., (Или задержки от каждого входа A/B/C до выхода...)
Пример выполнения всех трех для сравнения нескольких коротких последовательностей см. В моем ответе " Как эффективный способ подсчета установленных битов в позиции или ниже?
Для коротких циклов современные ЦП имеют достаточно ресурсов для выполнения не по порядку (физический размер файла регистров, поэтому переименование не заканчивается из-за регистров, размер ROB), чтобы иметь достаточно итераций цикла в полете, чтобы найти весь параллелизм. Но по мере того, как цепочки зависимостей внутри циклов становятся длиннее, в конце концов они заканчиваются. См. Измерение емкости буфера переупорядочения для получения подробной информации о том, что происходит, когда у ЦП заканчиваются регистры для переименования.
Смотрите также много ссылок на производительность и ссылки в вики тега x86.
Настройка вашего цикла FMA:
Да, точка-продукт на Haswell будет узким местом в пропускной способности L1D при половине пропускной способности модулей FMA, так как он требует двух нагрузок на умножение + сложение.
Если бы вы делали B[i] = x * A[i] + y;
или же sum(A[i]^2)
Вы можете насытить пропускную способность FMA.
Похоже, что вы все еще пытаетесь избежать повторного использования регистра даже в случаях только для записи, таких как назначение vmovaps
загрузить, поэтому вы исчерпали регистры после развертывания на 8. Это хорошо, но может иметь значение для других случаев.
Кроме того, используя ymm8-15
может немного увеличить размер кода, если это означает, что требуется 3-байтовый префикс VEX вместо 2-байтового. Интересный факт: vpxor ymm7,ymm7,ymm8
нужен 3-байтовый VEX в то время как vpxor ymm8,ymm8,ymm7
нужен только 2-байтовый префикс VEX. Для коммутативных операций сортируйте regs источника от высокого до низкого.
Наше узкое место в нагрузке означает, что пропускная способность FMA в лучшем случае составляет половину от максимальной, поэтому нам нужно как минимум 5 векторных аккумуляторов, чтобы скрыть их задержку. 8 хорошо, так что в цепочках зависимостей есть много провалов, чтобы они могли наверстать упущенное после любых задержек из-за неожиданной задержки или конкуренции за p0/p 1. 7 или, может быть, даже 6 тоже подойдут: ваш фактор разворота не должен быть степенью 2.
Развертывание ровно на 5 будет означать, что вы также находитесь в узком месте для цепочек зависимостей. Каждый раз, когда FMA не запускается в точном цикле, его ввод готов означает потерянный цикл в этой цепочке зависимостей. Это может произойти, если загрузка медленная (например, она отсутствует в кеше L1 и должна ждать L2), или если загрузка завершается не в порядке, и FMA из другой цепочки зависимостей крадет порт, для которого было запланировано FMA. (Помните, что планирование происходит во время выпуска, так что мопы, сидящие в планировщике, являются либо FMA порта 0, либо FMA порта 1, а не FMA, который может принимать тот порт, который свободен).
Если вы оставите некоторую слабость в цепочках зависимостей, выполнение вне порядка может "догнать" FMA, поскольку они не будут узкими местами по пропускной способности или задержке, просто ожидая результатов загрузки. @Forward обнаружил (в обновлении вопроса), что развертывание на 5 снижает производительность с 93% пропускной способности L1D до 89,5% для этого цикла.
Я предполагаю, что развертка на 6 (на единицу больше, чем минимум, чтобы скрыть задержку) будет в порядке, и будет примерно такая же производительность, как и развертка на 8. Если бы мы были ближе к максимальному увеличению пропускной способности FMA (а не просто к узким местам при загрузке) пропускной способности), одного больше минимального может быть недостаточно.
обновление: экспериментальный тест @Forward показывает, что мое предположение было неверным. Нет большой разницы между unroll5 и unroll6. Кроме того, unroll15 вдвое ближе, чем unroll8, к теоретической максимальной пропускной способности 2x 256b нагрузок на такт. Измерение только с независимыми нагрузками в контуре или с независимыми нагрузками и FMA только для регистров, скажет нам, сколько из этого происходит из-за взаимодействия с цепочкой зависимостей FMA. Даже в лучшем случае не получится идеальная пропускная способность 100%, хотя бы из-за ошибок измерения и сбоев из-за прерываний таймера. (Linux perf
измеряет только циклы пользовательского пространства, если вы не запускаете его как root, но время по-прежнему включает время, потраченное на обработчики прерываний. Вот почему частота вашего процессора может быть сообщена как 3,87 ГГц при работе от имени пользователя root, но 3,900 ГГц при запуске от имени пользователя root и измерения. cycles
вместо cycles:u
.)
Мы не ограничиваемся пропускной способностью внешнего интерфейса, но мы можем уменьшить количество мопов в слитых доменах, избегая индексированных режимов адресации для mov
инструкции. Чем меньше, тем лучше и делает это более дружественным к гиперпоточности, когда делится ядром с чем-то другим, кроме этого.
Простой способ - просто сделать два приращения указателя внутри цикла. Сложный способ - это аккуратный способ индексации одного массива относительно другого:
;; input pointers for x[] and y[] in rdi and rsi
;; size_t n in rdx
;;; zero ymm1..8, or load+vmulps into them
add rdx, rsi ; end_y
; lea rdx, [rdx+rsi-252] to break out of the unrolled loop before going off the end, with odd n
sub rdi, rsi ; index x[] relative to y[], saving one pointer increment
.unroll8:
vmovaps ymm0, [rdi+rsi] ; *px, actually py[xy_offset]
vfmadd231ps ymm1, ymm0, [rsi] ; *py
vmovaps ymm0, [rdi+rsi+32] ; write-only reuse of ymm0
vfmadd231ps ymm2, ymm0, [rsi+32]
vmovaps ymm0, [rdi+rsi+64]
vfmadd231ps ymm3, ymm0, [rsi+64]
vmovaps ymm0, [rdi+rsi+96]
vfmadd231ps ymm4, ymm0, [rsi+96]
add rsi, 256 ; pointer-increment here
; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
; smaller code-size helps in the big picture, but not for a micro-benchmark
vmovaps ymm0, [rdi+rsi+128-256] ; be pedantic in the source about compensating for the pointer-increment
vfmadd231ps ymm5, ymm0, [rsi+128-256]
vmovaps ymm0, [rdi+rsi+160-256]
vfmadd231ps ymm6, ymm0, [rsi+160-256]
vmovaps ymm0, [rdi+rsi-64] ; or not
vfmadd231ps ymm7, ymm0, [rsi-64]
vmovaps ymm0, [rdi+rsi-32]
vfmadd231ps ymm8, ymm0, [rsi-32]
cmp rsi, rdx
jb .unroll8 ; } while(py < endy);
Использование неиндексированного режима адресации в качестве операнда памяти для vfmaddps
позволяет ему оставаться в микроплавленом состоянии в ядре, вышедшем из строя, вместо того, чтобы быть неслоистым. Режимы микросинтеза и адресации
Так что мой цикл - это 18 мопов слитых доменов для 8 векторов. Yours берет 3 uops из fused-domain для каждой пары vmovaps + vfmaddps вместо 2 из-за отсутствия ламинирования индексированных режимов адресации. У обоих из них, конечно, по-прежнему есть 2 загрузки в неиспользованном домене (port2/3) на пару, так что это все еще узкое место.
Меньшее количество мопов в слитых доменах позволяет выполнять в неупорядоченном порядке больше итераций, что потенциально помогает лучше справляться с промахами в кеше. Это незначительная вещь, когда мы находимся в узком месте на исполнительном модуле (в этом случае загружаем мопы) даже без промахов кеша. Но с гиперпоточностью вы получаете только каждый второй цикл пропускной способности внешнего интерфейса, если другой поток не остановлен. Если он не слишком сильно конкурирует за нагрузку и p0/1, меньшее количество мопов в слитых доменах позволит этому циклу работать быстрее при совместном использовании ядра. (например, может быть, другой гипер-поток работает с большим количеством портов 5 / port6 и хранит мопы?)
Поскольку после uop-кэша происходит расслоение, ваша версия не занимает дополнительное место в кэше uop. Disp32 с каждым мопом в порядке и не занимает дополнительного места. Но более крупный размер кода означает, что uop-кеш менее вероятно будет упакован так же эффективно, так как вы достигнете границ 32B, прежде чем строки uop-кеша будут заполняться чаще. (На самом деле, меньший код также не гарантирует лучшего. Меньшие инструкции могут привести к заполнению строки кэша UOP и необходимости одной записи в другой строке перед пересечением границы 32B.) Этот небольшой цикл может выполняться из буфера обратной связи (LSD), поэтому к счастью, uop-кеш не является фактором.
Затем после цикла: Эффективная очистка является сложной частью эффективной векторизации для небольших массивов, которые могут не быть кратными коэффициенту развертывания или особенно ширине вектора.
...
jb
;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.
; reduce down to a single vector, with a tree of dependencies
vaddps ymm1, ymm2, ymm1
vaddps ymm3, ymm4, ymm3
vaddps ymm5, ymm6, ymm5
vaddps ymm7, ymm8, ymm7
vaddps ymm0, ymm3, ymm1
vaddps ymm1, ymm7, ymm5
vaddps ymm0, ymm1, ymm0
; horizontal within that vector, low_half += high_half until we're down to 1
vextractf128 xmm1, ymm0, 1
vaddps xmm0, xmm0, xmm1
vmovhlps xmm1, xmm0, xmm0
vaddps xmm0, xmm0, xmm1
vmovshdup xmm1, xmm0
vaddss xmm0, xmm1
; this is faster than 2x vhaddps
vzeroupper ; important if returning to non-AVX-aware code after using ymm regs.
ret ; with the scalar result in xmm0
Для получения дополнительной информации о горизонтальной сумме в конце см. Самый быстрый способ сделать горизонтальную векторную сумму с плавающей точкой на x86. Два 128-битных шаффла, которые я использовал, даже не нуждаются в непосредственном байте управления, поэтому он экономит 2 байта размера кода по сравнению с более очевидным shufps
, (И 4 байта размера кода против vpermilps
, потому что этот код операции всегда нуждается в 3-байтовом префиксе VEX, а также немедленном). AVX с 3 операндами очень хорош по сравнению с SSE, особенно при написании на C со встроенными функциями, поэтому вы не можете так просто выбрать холодный регистр для movhlps
в.