Почему mulss принимает только 3 цикла на Haswell, отличаясь от таблиц инструкций Agner?
Я новичок в оптимизации инструкций.
Я провел простой анализ простой функции 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;
}
Я использую тестовый кадр, предоставленных палочек agner туман на веб - testp.
Массивы, которые используются в этом случае, выровнены:
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 упс, в согласии с анализом.
Номера uops, выполняемых в port0 port1 port5 port6, похожи на то, что говорит анализ. Я думаю, что, возможно, планировщик 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 * 256bit/8.59, он очень близок к пику 2*256/8, использование составляет около 93%, только единица FMA использованный 8/8. 59, пик 2*8/8, использование 47%.
Итак, я думаю, что достиг узкого места L1D, как и ожидал Питер Кордес.==================================================================
Особая благодарность Боанну, исправившему так много грамматических ошибок в моем вопросе.=================================================================
Из ответа Питера я понял, что только регистр" чтение и запись "будет зависимостью," только писатель" регистры не были бы зависимостью.
Поэтому я пытаюсь уменьшить регистры, используемые в цикле, и я пытаюсь развернуть на 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 в web "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
задержке.Необязательное чтение: в терминах компьютерной архитектуры: переименование регистра позволяет избежатьопасности войны с зависимостями данных повторного использования одного и того же архитектурного регистра. (Некоторые конвейеризация + схемы отслеживания зависимостей до переименования регистра не решали всех проблем, поэтому область компьютерной архитектуры делает большое дело из различных видов угроз для данных.
Переименование регистра с помощью алгоритмаТомасуло удаляет все, кроме фактических истинных зависимостей (чтение после записи), поэтому любая инструкция, где адресат не является также исходным регистром, не взаимодействует с цепочкой зависимостей, включающей старое значение этого регистра. зарегистрировать. (За исключением ложных зависимостей, таких как
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 цикла на Хасвелл.rax
, читается и пишетсяadd rax, 1
. Задержка 1С, так что это не критический путь.Похоже, что вы правильно измерили время выполнения / количество циклов, потому что узкие места цикла на задержке 3c
addss
.Это ожидаемо: последовательная зависимость в точечном произведении-это сложение в единую сумму (она же редукция), а не умножение между векторными элементами.
Это, безусловно, доминирующее узкое место для этого цикла, несмотря на различные незначительные недостатки:
short i
произвел глупыйcmp cx, ax
, который принимает дополнительный префикс размера операнда. К счастью, gcc удалось избежать фактического выполненияadd ax, 1
, потому что signed-overflow-это неопределенное поведение в C. , поэтому оптимизатор может предположить, что этого не происходит. (обновление: целочисленные правила продвижения делают его другим дляshort
, таким образом, UB не входит в него, но gcc все еще может легально оптимизировать. Довольно странная штука.)Если вы компилировали с
Я не уверен, почему у вас есть-mtune=intel
, или лучше,-march=haswell
, gcc поместил быcmp
иjg
рядом друг с другом, где они могли бы макросвариться.*
в вашей таблице наcmp
иadd
Инструкции. (обновление: я просто предполагал, что вы используете нотацию, как IACA, но, по-видимому, это не так). Ни один из них не сливается. Единственное слияние происходит микро-слияниеmulss xmm1, [rsi+rax*4]
.И поскольку это 2-операндная инструкция ALU с назначением read-modify-write регистр, он остается макро-сплавленным даже в робе на Хасвелле. (Сэндибридж распаковал бы его в момент выпуска.) обратите внимание, что
vmulss xmm1, xmm1, [rsi+rax*4]
распластался бы и на Хасвелле.Все это на самом деле не имеет значения, так как вы просто полностью ограничиваете задержку FP-add, намного медленнее, чем любые ограничения пропускной способности uop. Без
-ffast-math
компиляторы ничего не могут сделать. С-ffast-math
, clang обычно развернется с несколькими аккумуляторами, и он будет автоматически векторизован, так что они будут векторными аккумуляторами. Таким образом, вы, вероятно, можете насытить предел пропускной способности Haswell в 1 вектор или скалярный FP add за такт, если вы попали в кэш L1D.С задержкой FMA 5c и пропускной способностью 0.5 c на Haswell, вам понадобятся 10 аккумуляторов, чтобы поддерживать 10 FMA в полете и максимизировать пропускную способность FMA, сохраняя P0 / p1 насыщенными FMA. (Взглянув снижается ФМА задержки до 4 циклов, и выполняется умножение, сложение, и ФМА на ФМА единиц. Таким образом, он на самом деле имеет более высокую задержку добавления, чем Haswell.)
(вы узкое место на нагрузки, потому что вам нужно две нагрузки для каждого FMA. В других случаях вы можете фактически получить дополнительную пропускную способность, заменив некоторую инструкцию a
vaddps
на FMA с множителем 1.0. Это означает большую задержку для скрытия, поэтому лучше всего в более сложном алгоритме, где у вас есть добавление, которое не находится на критическом пути в первую очередь.)
Re: uops per port :
Да, что-то вроде этого. Uops не назначаются случайным образом или каким-то образом равномерно распределены по каждому порту, на котором они могли бы работать. Вы предположили, чтоЕсть 1.19 uops на петлю в Порту 5, это намного больше, чем ожидалось 0.5, не так ли о диспетчере uops, пытающемся сделать uops на каждом порту одинаковым
add
иcmp
uops будут равномерно распределены по p0156, но это не так.Этап выдачи назначает uops портам на основе того, сколько uops уже ожидают этот порт. Так как
addss
может работать только на p1 (и это узкое место цикла), обычно есть много uops p1, выпущенных, но не выполненных. Так мало других uops когда-либо будут запланированы к port1. (Это включает в себяmulss
: Большинствоmulss
uops в конечном итоге запланированы на порт 0.)Принято-ветви могут работать только на Порту 6. Порт 5 не имеет никаких uops в этом цикле, которые могуттолько работать там, поэтому он в конечном итоге привлекает много многопортовых uops.
Планировщик (который выбирает unfused-domain uops из станции резервирования) недостаточно умна для запуска критического пути, поэтому этот алгоритм назначения уменьшает задержку конфликта ресурсов (другие uops крадут port1 на циклах, когда
addss
мог бы работать). Это также полезно в тех случаях, когда вы ограничиваете пропускную способность данного порта.Планирование уже назначенных uops обычно является самым старым-готовым первым, как я понимаю. Этот простой алгоритм вряд ли удивителен, так как он должен выбрать uop с его входами, готовыми для каждого порта из A 60-entry RS каждый такт, не плавя ваш процессор. Вышедший из строя механизм, который находит и эксплуатирует ILP, является одним из значительных затрат энергии в современном процессоре, сравнимых с исполнительными блоками, которые выполняют фактическую работу.
Related / more details: как именно запланированы x86 uops?
Еще анализ производительности:
Кроме пропусков кэша / неверных предсказаний ветвей, три основных возможных узких места для циклов, связанных с ЦП являются:
- цепочки зависимостей (как в этом случае)
- пропускная способность переднего плана (максимум 4 uops плавленого домена, выдаваемых за часы на Haswell)
- узкие места порта выполнения, например, если многим UOP требуется p0/p1 или p2/p3, как в развернутом цикле. Count unfused-домен uops для определенных портов. Как правило, вы можете предполагать наилучшее распределение, с uops, которые могут работать на других портах, не крадя занятые порты очень часто,но это действительно происходит.
Тело петли или короткий блок кода может быть приблизительно охарактеризован тремя вещами: подсчет uop плавленого домена, подсчет unfused-домена, на котором он может выполняться, и общая задержка критического пути, предполагающая наилучшее планирование для его критического пути. (Или задержки от каждого из входных A/B/C к выходу...)
Например, для сравнения всех трех коротких последовательностей см. Мой ответ на каков эффективный способ подсчета битов набора в позиции или ниже?
Для краткости циклы, современные процессоры имеют достаточно ресурсов для выполнения вне очереди (размер файла физического регистра, чтобы переименование не заканчивалось регистрами, размер Роба), чтобы иметь достаточно итераций цикла в полете, чтобы найти весь параллелизм. Но по мере того, как цепочки зависимостей внутри петель становятся длиннее, в конце концов они заканчиваются. См. измерение емкости буфера переупорядочения для получения некоторых подробностей о том, что происходит, когда процессор исчерпывает регистры для переименования.
См. также множество ссылок на производительность и ссылки в разделе x86 tag wiki.
Настройка цикла FMA:
Да, dot-продукт на Haswell будет узким местом на пропускной способности L1D только в половине пропускной способности блоков FMA, так как он принимает две нагрузки на multiply+add.
Если бы вы делали
B[i] = x * A[i] + y;
илиsum(A[i]^2)
, вы могли бы насытить пропускную способность FMA.Похоже, что вы все еще пытаетесь избежать повторного использования регистра даже в случаях только для записи, таких как назначение загрузки
vmovaps
, поэтому у вас закончились регистры после разворачивание по 8 . Это прекрасно, но может иметь значение для других случаев.Кроме того, использование
ymm8-15
может немного увеличить размер кода, если это означает, что вместо 2-байтового префикса VEX требуется 3-байтовый префикс. Забавный факт:vpxor ymm7,ymm7,ymm8
нужен 3-байтовый VEX, в то время какvpxor ymm8,ymm8,ymm7
нужен только 2-байтовый префикс VEX. Для коммутативных операций сортируйте исходные правила от максимума до минимума.Наше узкое место нагрузки означает, что пропускная способность FMA в лучшем случае вдвое меньше максимальной, поэтому нам нужно как минимум 5 векторных аккумуляторов, чтобы скрыть их задержку. 8-это хорошо, так что в цепочках зависимостей достаточно слабины, чтобы они могли догнать их после любых задержек из-за неожиданной задержки или конкуренции за p0/p1. 7 или, может быть, даже 6 тоже было бы хорошо: ваш коэффициент разворачивания не должен быть в степени 2.
Разворачивание ровно на 5 будет означать, что вы также находитесь в узком месте для цепочек зависимостей. Всякий раз, когда FMA не работает в точном цикле, его вход готов, означает потерянный цикл в этой цепочке зависимостей. Это может произойти, если нагрузка медленная (например, он пропускает в кэше L1 и должен ждать L2), или если загрузка завершается не по порядку и FMA из другой цепочки зависимостей крадет порт, для которого этот FMA был запланирован. (Помните, что планирование происходит во время выпуска, поэтому uops, сидящие в планировщике, являются либо port0 FMA, либо port1 FMA, а не FMA, который может принимать любой порт, находящийся в режиме ожидания).
Если вы оставите некоторую слабину в цепочках зависимостей, внепорядковое выполнение может "догнать" FMA, потому что они не будут узким местом на пропускная способность или задержка, просто ожидание результатов загрузки. @Forward обнаружил (в обновлении к вопросу), что развертывание на 5 снизило производительность с 93% пропускной способности L1D до 89,5% для этого цикла.
Я предполагаю, что размотка на 6 (на один больше, чем минимум, чтобы скрыть задержку) была бы здесь в порядке, и получить примерно такую же производительность, как и размотка на 8. Если бы мы были ближе к максимальному значению пропускной способности FMA (а не просто узким местом на пропускной способности нагрузки), один больше, чем минимум, не мог бы быть достаточно.
Update: экспериментальный тест @Forward показывает, что мое предположение было неверным . Нет большой разницы между unroll5 и unroll6. Кроме того, unroll15 в два раза ближе, чем unroll8, к теоретической максимальной пропускной способности 2x 256b нагрузок в сутки. Измерение только с независимыми нагрузками в цикле, или с независимыми нагрузками и только регистром FMA, показало бы нам, насколько это связано с взаимодействием с цепью зависимостей FMA. Даже самый лучший случай не будет идеальным 100% пропускная способность, хотя бы из-за ошибок измерения и сбоев из-за прерываний таймера. (Linux
perf
измеряет только циклы пользовательского пространства, если вы не запускаете его от имени root, но время все еще включает время, проведенное в обработчиках прерываний. Вот почему частота вашего процессора может быть указана как 3,87 ГГц при запуске от имени некорня, но 3,900 ГГц при запуске от имени корня и измеренииcycles
вместоcycles:u
.)
Мы не являемся узким местом на интерфейсной пропускной способности, но мы можем уменьшить количество uop плавленого домена на избегание индексированных режимов адресации для не -
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 uops плавленого домена для 8 векторов. Для каждой пары vmovaps + vfmaddps требуется 3 uops с плавленым доменом вместо 2 из-за отсутствия слоения индексированных режимов адресации. Оба из них все еще, конечно, имеют 2 unfused-domain load uops (port2/3) на пару, так что это все еще узкое место.
Меньшее количество uops с плавленым доменом позволяет выполнять вне очереди смотрите больше итераций впереди, потенциально помогая ему лучше поглощать промахи кэша. Это незначительная вещь, когда мы находимся в узком месте на исполнительном блоке (load uops в этом случае)даже без промахов кэша. Но с hyperthreading вы получаете только каждый второй цикл пропускной способности front-end issue, если только другой поток не остановлен. Если он не слишком конкурирует за нагрузку и p0 / 1, меньшее количество UOP с плавленым доменом позволит этому циклу работать быстрее при совместном использовании ядра. (например, может быть, другой гиперпоток работает много port5 / port6 и магазин uops?)
Поскольку un-lamination происходит после uop-кэша, ваша версия не занимает дополнительного места в кэше uop. Disp32 с каждым uop в порядке и не занимает лишнего места. Но более объемный размер кода означает, что 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 . Пара 128b shuffles, которые я использовал, даже не нуждаются в непосредственном контрольном байте, поэтому он экономит 2 байта кода по сравнению с более очевидным
shufps
. (И 4 байта размера кода противvpermilps
, потому что этот код операции всегда требует 3-байтового префикса VEX, а также немедленного). AVX 3-операндный материал очень хорош по сравнению с SSE, особенно при написании на C с внутренними компонентами, поэтому вы не можете так же легко выбрать холодный регистр дляmovhlps
.