Почему mulss занимает всего 3 цикла на Хасуэлле, отличном от таблиц инструкций Агнера?
Я новичок в оптимизации инструкций.
Я сделал простой анализ простой функции dotp, которая используется для получения точечного произведения двух массивов с плавающей запятой.
Код C выглядит следующим образом:
- Как я могу перечислить все загруженные сборки?
- Как скомпилировать и запустить программу C в Sublime Text 2?
- Очень быстро memcpy для обработки изображений?
- .NET Assembly Diff / Compare Tool - Что доступно?
- x86_64 - Условия сборки и выход из строя
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 Fog на веб- тест .
Массивы, которые используются в этом случае, выровнены:
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 port 5 port6, похоже на то, что говорит анализ. Я думаю, возможно, планировщик uops делает это, он может попытаться сбалансировать нагрузки на порты, я прав?
Я совершенно не понимаю, почему существует только около 3 циклов за цикл. Согласно таблице инструкций mulss
, латентность команды 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, которого ожидает Питер Кордес.
================================================== ================
Особая благодарность Боанну, исправить так много грамматических ошибок в моем вопросе.
================================================== ===============
Из ответа Питера я понимаю, что только «прочитанный и написанный» регистр будет зависимостью, «записи только для писателя» не будут зависимостью.
Поэтому я пытаюсь уменьшить регистры, используемые в цикле, и я пытаюсь развернуть на 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
- Примеры предварительной выборки?
- Как получить вывод ассемблера из источника C / C ++ в gcc?
- mscorlib.dll & System.dll
- Maven: добавьте зависимость к банке относительным путем
- Проверьте, равен ли регистр нулю с помощью CMP reg, 0 против OR reg, reg?
- Определите, были ли сборки .NET построены из одного источника
- Дополнительная информация о макете памяти исполняемой программы (процесса)
- Как загрузить сборку в AppDomain со всеми ссылками рекурсивно?
Посмотрите на свой цикл еще раз: movss xmm1, src
не зависит от старого значения xmm1
, поскольку его назначение – только для записи . mulss
каждого итерации является независимой. Выполнение вне порядка может и может использовать этот параллелизм на уровне инструкций, поэтому вы определенно не 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-хaddss xmm0, xmm1
задержку на Haswell. -
rax
, читать и писатьadd rax, 1
. 1c, поэтому это не критический путь.
Похоже, вы правильно измерили время выполнения / цикл-счет, потому что узкие места цикла addss
задержкой 3c addss
.
Ожидается, что последовательная зависимость в точечном продукте является добавлением в одну сумму (ака сокращение), а не умножения между векторными элементами.
Это, безусловно, является доминирующим узким местом для этого цикла, несмотря на различные незначительные неэффективности:
short i
произвел глупый cmp cx, ax
, который принимает дополнительный префикс размера операнда. К счастью, gcc удалось избежать фактического add ax, 1
, потому что переполнение с подписью – Undefined Behavior в C. Поэтому оптимизатор может предположить, что этого не происходит . (обновление: целые правила продвижения делают это по-другому , поэтому UB не входит в него, но gcc все еще может легально оптимизировать. Довольно дурацкие вещи.)
Если вы скомпилировали с помощью -mtune=intel
или, лучше, -march=haswell
, gcc поставил бы cmp
и jg
рядом друг с другом, где они могли бы -march=haswell
.
Я не уверен, почему у вас есть *
в вашей таблице на cmp
и add
инструкции. (обновление: я просто догадывался, что вы использовали нотацию, такую как IACA , но, видимо, вы не были). Ни один из них не сливается. Единственное слияние – микроплавление mulss xmm1, [rsi+rax*4]
.
И поскольку это инструкция с 2-операндами 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 / p1 насыщенным FMA. (Skylake уменьшает задержку FMA до 4 циклов и запускает умножение, добавление и FMA на устройства FMA, поэтому на самом деле имеет более высокую задержку добавления, чем Haswell).
(У вас есть узкие места при загрузке, потому что для каждой FMA вам нужны две нагрузки. В других случаях вы можете фактически увеличить пропускную способность, заменив некоторую vaddps
на FMA с коэффициентом умножения 1.0. Это означает, что для скрытия требуется больше времени ожидания, поэтому это лучше всего в более сложном алгоритме, где у вас есть добавление, которое не находится на критическом пути в первую очередь.)
Re: uops за порт :
в порте 5 есть 1,19 мкп за цикл, это намного больше, чем ожидалось 0.5, это вопрос о диспетчере uops, пытающемся сделать uops на каждом порту одинаковым
Да что-то подобное.
Уопы не назначаются случайным образом или равномерно распределены по каждому порту, на котором они могут работать. Вы предположили, что add
и cmp
uops будут равномерно распределяться через p0156, но это не так.
Этап выпуска присваивает портам uops в зависимости от того, сколько uops уже ожидает этого порта. Так как addss
может работать только на p1 (и это узкое место цикла), обычно выдается много p1 uops, но не выполняется. Таким образом, немногие другие устройства будут назначены на порт1. (Это включает в себя mulss
: большая часть mulss
конечном итоге запланирована на порт 0.)
Взятые ветви могут работать только на порте 6. Порт 5 не имеет в этом цикле никаких совпадений, которые могут работать только там, поэтому он привлекает много многопортовых устройств.
Планировщик (который выбирает неустановленные домены из резервной станции) недостаточно умен, чтобы запускать критический путь вперёд, так что это алгоритм присваивания уменьшает латентность ресурсов-конфликтов (другие uops крадут порт1 в циклы, когда addss
может иметь бег). Это также полезно в тех случаях, когда вы сталкиваетесь с пропускной способностью данного порта.
Планирование уже назначенных uops, как я понимаю, обычно старее всего готово. Этот простой алгоритм вряд ли удивителен, так как он должен выбрать uop со своими входами, готовыми для каждого порта с 60-входом RS каждый такт, без таяния вашего процессора. Механизм не по назначению, который находит и использует ILP, является одной из значительных затрат на электроэнергию в современном процессоре, сравнимой с исполнительными единицами, которые выполняют фактическую работу.
Связанные / более подробная информация: как запланировано x86 uops?
Более эффективный анализ:
Помимо недостатков промахов / ветвей кэша, три основных возможных узких места для циклов, связанных с процессором, следующие:
- цепи зависимостей (как в этом случае)
- пропускная способность переднего плана (макс. 4-х разовых доменов, выпущенных за часы на Haswell)
- узкие места в порте выполнения, например, если для большого количества uops требуется p0 / p1 или p2 / p3, как в вашем развернутом цикле. Считайте unused-domain uops для определенных портов. Как правило, вы можете предполагать распределение наилучшего случая, с помощью uops, которые могут работать на других портах, не очень часто воруя занятые порты, но это действительно так.
Тело цикла или короткий блок кода могут быть приблизительно охарактеризованы тремя вещами: счетчиком uop с плавным доменом, количеством неиспользуемых доменов, из которых могут выполняться исполняемые модули, и полной задержкой критического пути, предполагающей наилучшее планирование для его критического пути , (Или задержки с каждого входа A / B / C на выход …)
Например, чтобы сделать все три, чтобы сравнить несколько коротких последовательностей, см. Мой ответ о том, что является эффективным способом подсчета бит в позиции или ниже?
Для коротких циклов современные процессоры имеют достаточно ресурсов вне очереди (размер физического регистра, поэтому для переименования не хватает регистров, размер ROB), чтобы иметь достаточно итераций цикла в полете, чтобы найти все параллелизм. Но поскольку цепи зависимостей внутри циклов становятся длиннее, в конечном итоге они заканчиваются. См. « Измерение емкости буфера переупорядочения» для получения подробной информации о том, что происходит, когда ЦПУ заканчивает переименование регистров.
См. Также много ссылок на производительность и ссылки в вики-теге x86 .
Настройка вашей петли FMA:
Да, точечный продукт на Haswell будет узким местом по пропускной способности L1D только в половине пропускной способности блоков FMA, так как он принимает две нагрузки на умножение + добавление.
Если вы делали B[i] = x * A[i] + y;
или sum(A[i]^2)
, вы можете насытить пропускную способность FMA.
Похоже, вы по-прежнему пытаетесь избежать повторного использования в реестре даже в случаях с записью, таких как назначение загрузки vmovaps
, поэтому у вас закончились регистры после разворачивания на 8 . Это нормально, но может иметь значение для других случаев.
Кроме того, использование ymm8-15
может немного увеличить размер кода, если это означает, что вместо 2-байтного байта необходим трехбайтовый префикс VEX. vpxor ymm7,ymm7,ymm8
факт: vpxor ymm7,ymm7,ymm8
нуждается в 3- vpxor ymm8,ymm8,ymm7
VEX, в то время как vpxor ymm8,ymm8,ymm7
нужен только 2-байтовый префикс VEX. Для коммутативных опций сортируйте исходные регистры с высокой до низкой.
Наше узкое место загрузки означает, что максимальная пропускная способность FMA составляет половину от максимального, поэтому нам нужно как минимум 5 векторных аккумуляторов, чтобы скрыть их латентность. 8 – это хорошо, поэтому в цепочках зависимостей есть много провисания, чтобы они могли догнать их после любых задержек от неожиданной латентности или конкуренции за p0 / p1. 7 или, может быть, даже 6 будет хорошо: ваш коэффициент разворота не должен иметь силу 2.
Развертывание ровно 5 означает, что вы также правы на узком месте для цепей зависимостей . Каждый раз, когда FMA не работает в точном цикле, его вход готов – означает потерянный цикл в этой цепочке зависимостей. Это может произойти, если нагрузка медленная (например, она пропускает в кеше L1 и должна ждать L2), или если нагрузки заканчиваются не в порядке, а FMA из другой сети зависимостей крадет порт, на который запланирован FMA. (Помните, что планирование происходит во время проблемы, поэтому в диспетчере, который находится в планировщике, есть либо порт FMA, либо порт 1 FMA, а не FMA, который может принимать любой порт в режиме ожидания).
Если вы оставите некоторое отставание в цепочках зависимостей, выполнение вне порядка может «догнать» FMA, потому что они не будут ограничены пропускной способностью или задержкой, просто ожидая результатов загрузки. @Forward нашла (в обновлении вопроса), что разворачивание на 5 уменьшило производительность с 93% от пропускной способности L1D до 89,5% для этого цикла.
Я предполагаю, что развернуть на 6 (один больше, чем минимум, чтобы скрыть латентность) будет здесь хорошо, и получить примерно такую же производительность, как развернуть на 8. Если бы мы были ближе к максимальному увеличению пропускной способности FMA (а не просто узким местом при загрузке пропускной способности), еще одного, чем минимум, может быть недостаточно.
обновление: экспериментальный тест в прямом эфире показывает, что моя догадка была неправильной . Между unroll5 и unroll6 нет большой разницы. Кроме того, unroll15 в два раза ближе, чем unroll8, до теоретической максимальной пропускной способности 2x 256 бит нагрузки за такт. Измерение только с независимыми нагрузками в контуре или с независимыми нагрузками и только с регистрами FMA могло бы рассказать нам, сколько из-за взаимодействия с цепочкой зависимостей FMA. Даже самый лучший случай не получит идеальной 100% -ной пропускной способности, хотя бы из-за ошибок измерения и сбоев из-за прерываний таймера. (Linux perf
измеряет только циклы пользовательского пространства, если вы не запускаете его как root, но время по-прежнему включает время, затрачиваемое на обработчики прерываний. Вот почему ваша частота процессора может быть указана как 3,87 ГГц при запуске как не root, а 3,900 ГГц при запуске как корневые и измерительные cycles
вместо cycles:u
.)
Мы не испытываем недостатка в пропускной способности интерфейса, но мы можем сократить счетчик uop с плавным доменом, избегая индексированных режимов адресации для не- mov
инструкций. Меньше, чем лучше, и делает это более удобной для гиперstreamов при совместном использовании ядра с чем-то другим, кроме этого.
Простым способом является просто выполнить два указателя-указателя внутри цикла. Сложный способ – аккуратный трюк индексации одного массива относительно другого:
;; 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 fused-domain uops для 8 векторов. Для каждой пары vmovaps + vfmaddps вместо 2 требуется 3 плавных домена, из-за отсутствия ламинирования индексированных режимов адресации. У обоих из них все еще есть 2 неуправляемых домена uops (port2 / 3) на пару, так что это все еще узкое место.
Меньшее количество отказов с плавным доменом позволяет выйти из строя, чтобы увидеть больше итераций вперед, потенциально помогая ему поглощать кеш-память лучше. Это незначительная вещь, когда мы испытываем узкое место в исполнительном модуле (в этом случае загружаем uops) даже без пропусков в кэше. Но с помощью hyperthreading вы получаете только каждый цикл полосы пропускания переднего плана, если другой stream не застопорился. Если он не слишком сильно конкурирует с нагрузкой и p0 / 1, меньшее количество консолей с объединенными доменами позволит этому циклу работать быстрее при совместном использовании ядра. (например, может быть, в другом гиперstreamе работает много port5 / port6 и хранилище?)
Поскольку un-lamination происходит после uop-cache, ваша версия не занимает дополнительного места в кэше uop. Disp32 с каждым uop в порядке и не занимает лишнего места. Но более объемный размер кода означает, что uop-cache с меньшей вероятностью будет паковать так же эффективно, так как вы попадете в границы 32B до того, как строки кэша uop будут заполнены чаще. (На самом деле, меньший код также не гарантирует лучшего. Меньшие инструкции могут привести к заполнению строки кэша uop и необходимости одной записи в другой строке до пересечения границы 32B.) Этот небольшой цикл может выполняться из буфера loopback (LSD), поэтому к счастью, uop-cache не является фактором.
Затем после цикла: Эффективная очистка - это трудная часть эффективной векторизации для небольших массивов, которая не может быть кратной коэффициенту разворота или особенно ширине вектора
... 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
Подробнее о горизонтальной сумме в конце см. В разделе Самый быстрый способ сделать горизонтальную векторную сумму float на x86 . Для двух татуировок 128b, которые я использовал, даже не требуется немедленный байт управления, поэтому он экономит 2 байта размера кода по сравнению с более очевидными shufps
. (И 4 байта по размеру кода против vpermilps
, потому что для этого кода операции всегда нужен 3-байтовый префикс VEX, а также немедленный). Файл AVX 3-operand очень хорош по сравнению с SSE, особенно при написании на C со встроенными movhlps
поэтому вы не можете так легко выбрать холодный регистр для movhlps
.