Почему 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 Fog в Интернете 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, в соответствии с анализом.

Количество uops, выполняемых в порте port0 port1 port 5 port6, похоже на то, что говорит анализ. Я думаю, возможно, планировщик uops делает это, он может попытаться сбалансировать нагрузки на порты, я прав?

Я совершенно не понимаю, почему существует только около 3 циклов за цикл. Согласно таблице agner , время ожидания команды 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%, он немного меньше, чем urolling на 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 независима. Выполнение вне порядка может и действительно использует этот уровень parallelism на уровне инструкций, поэтому вы определенно не будете узким местом в латентности mulss.

Дополнительное чтение: в терминах компьютерной архитектуры: переименование регистров позволяет избежать WAR-данных об опасностях, связанных с зависимостью, от повторного использования одного и того же архитектурного регистра. (Некоторые схемы конвейеризации + схемы отслеживания зависимостей до переименования реестров не решают всех проблем, поэтому область компьютерной архитектуры делает большую проблему из-за различных видов опасностей данных.

Зарегистрировать переименование с помощью алгоритм Tomasulo заставляет все уйти, кроме фактических истинных зависимостей (читать после записи), поэтому любая инструкция, также исходный регистр не имеет взаимодействия с цепочкой зависимостей, включающей старое значение этого регистра. (За исключением ложных зависимостей, например 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. 1c, поэтому это не критический путь.

Похоже, вы правильно измерили время выполнения/цикл, потому что узкие места цикла в латентности 3c addss.

Ожидается, что последовательная зависимость в точечном продукте является добавлением в одну сумму (ака сокращение), а не умножения между векторными элементами.

Это, безусловно, является доминирующим узким местом для этого цикла, несмотря на различные незначительные неэффективности:


short i создал глупую cmp cx, ax, которая принимает дополнительный префикс размера операнда. К счастью, gcc удалось избежать выполнения add ax, 1, потому что переполнение с подписью Undefined Поведение в C. Поэтому оптимизатор может предположить, что этого не происходит. (обновление: целые правила продвижения делают его другим для short, поэтому UB не входит в него, но gcc все еще может легально оптимизировать. Довольно дурацкие вещи.)

Если вы скомпилировали с помощью -mtune=intel или лучше, -march=haswell, gcc поместил бы cmp и jg рядом друг с другом, где они могли бы сглаживать макросы.

Я не уверен, почему у вас есть * в вашей таблице в инструкциях 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. В других случаях вы можете фактически увеличить пропускную способность, заменив некоторую инструкцию a vaddps на FMA с коэффициентом умножения 1.0. латентность, чтобы скрыть, поэтому лучше всего в более сложном алгоритме, где у вас есть добавление, а не на критический путь в первую очередь.)


Re: uops per port:

в порте 5 есть 1.19 uops за цикл, это намного больше, чем ожидалось 0.5, это вопрос о диспетчере uops, пытающемся сделать uops на каждом порту такого же

Да, что-то в этом роде.

Уопс не назначается случайным образом или равномерно распределяется по каждому порту, на котором они могут работать. Вы предположили, что add и cmp будут равномерно распределены по p0156, но это не так.

Этап выпуска назначает удаленные порты в зависимости от количества ожидающих этого порта. Поскольку addss может работать только на p1 (и это узкое место цикла), обычно выдается много p1 uops, но не выполняется. Таким образом, немногие другие устройства будут назначены на порт1. (Это включает в себя mulss: большая часть mulss uops в конечном итоге запланирована на порт 0.)

Взятые ветки могут выполняться только на порте 6. Порт 5 не имеет в этом цикле никаких совпадений, которые могут работать только там, поэтому он заканчивает привлекать много многопортовых устройств.

Планировщик (который выбирает неустановленные домены из резервной станции) недостаточно умен, чтобы запускать критический путь в первую очередь, поэтому этот алгоритм присваивания уменьшает латентность ресурсов (другие удаляют порт 1 по циклам, когда addss мог работать). Это также полезно в тех случаях, когда вы сталкиваетесь с пропускной способностью данного порта.

Планирование уже назначенных uops, как я понимаю, обычно старее всего готово. Этот простой алгоритм вряд ли удивителен, так как он должен выбрать uop со своими входами, готовыми для каждого порта из 60-entry RS каждый такт, без таяния вашего процессора. Механизм нарушенного порядка, который находит и использует ILP, является одной из значительных затрат на электроэнергию в современном процессоре, сравнимой с единицами исполнения, которые выполните фактическую работу.

Связанные/дополнительные сведения: Как запланировано x86 uops?


Дополнительные материалы анализа производительности:

Помимо недостатков промахов/ветвей кэша, три основных возможных узких места для циклов, связанных с процессором, следующие:

  • цепи зависимостей (как в этом случае)
  • пропускная способность переднего плана (макс. 4-х разобранных доменов, выпущенных за часы на Haswell)
  • узкие места выполнения портлетов, например, если вам нужно много ppm/p1 или p2/p3, как в вашем развернутом цикле. Считайте unused-domain uops для определенных портов. Как правило, вы можете предполагать распределение наилучшего случая, с помощью uops, которые могут запускаться на других портах, не очень часто воруя занятые порты, но это действительно происходит.

Тело цикла или короткий блок кода может быть приблизительно охарактеризовано тремя вещами: счетчиком uop с плавной областью, количеством неиспользуемых доменов, из которых могут выполняться исполняемые модули, и общей задержкой критического пути, предполагающей наилучшее планирование для его критический путь. (Или задержки с каждого входа A/B/C на выход...)

Например, чтобы сделать все три, чтобы сравнить несколько коротких последовательностей, см. мой ответ на Каков эффективный способ подсчета бит в позиции или ниже?

Для коротких циклов современные процессоры имеют достаточно ресурсов вне очереди (размер физического регистра, поэтому для переименования не хватает регистров, размер ROB), чтобы иметь достаточно итераций цикла в полете, чтобы найти все parallelism. Но поскольку цепи зависимостей внутри циклов становятся длиннее, в конечном итоге они заканчиваются. См. Измерение емкости буферизации порядка для получения подробной информации о том, что происходит, когда ЦПУ заканчивает переименовывать регистры.

См. также много ссылок на производительность и ссылки в теги wiki.


Настройка петли FMA:

Да, точечный продукт на Haswell будет узким местом на пропускной способности L1D только в половине пропускной способности блоков FMA, так как он принимает две нагрузки на умножение + добавление.

Если вы делали B[i] = x * A[i] + y; или sum(A[i]^2), вы могли бы насытить пропускную способность FMA.

Похоже, что вы по-прежнему пытаетесь избежать повторного использования регистра даже в случаях с записью, таких как назначение загрузки vmovaps, поэтому после разворота из-за 8 у вас закончились регистры. Это прекрасно, но может иметь значение для других случаев.

Кроме того, использование ymm8-15 может немного увеличить размер кода, если это означает, что вместо 2-байтного байта нужен 3-байтовый префикс VEX. Забавный факт: vpxor ymm7,ymm7,ymm8 нужен 3-байтовый VEX, а vpxor ymm8,ymm8,ymm7 нужен только 2-байтовый префикс VEX. Для коммутативных ops сортируйте исходные регистры с высокой до низкой.

Наше узкое место загрузки означает, что максимальная пропускная способность 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 (а не просто узкое место при загрузке нагрузки), еще один минимум может быть недостаточно.

обновление: экспериментальный тест @Forward показывает, что моя ошибка была неправильной. Между unroll5 и unroll6 нет большой разницы. Кроме того, unroll15 в два раза ближе, чем unroll8, до теоретической максимальной пропускной способности 2x 256 бит нагрузки за такт. Измерение только с независимыми нагрузками в контуре или с независимыми нагрузками и только с регистрами FMA могло бы рассказать нам, сколько из-за взаимодействия с цепочкой зависимостей FMA. Даже самый лучший случай не получит идеальной 100% -ной пропускной способности, хотя бы из-за ошибок измерения и сбоев из-за прерываний таймера. (Linux perf измеряет только циклы пользовательского пространства, если вы не запускаете его как root, но время по-прежнему включает время, затрачиваемое на обработчики прерываний. Вот почему ваша частота процессора может быть указана как 3,87 ГГц при запуске как не root, а 3.900 ГГц при запуске от имени root и измерения 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 fused-domain uops для 8 векторов. Для каждой пары vmovaps + vfmaddps вместо 2 требуется 3 плавных домена, из-за отсутствия ламинирования индексированных режимов адресации. У обоих из них все еще есть 2 неуправляемых домена uops (port2/3) на пару, так что все еще узкое место.

Меньшее количество фьюзических доменов uops позволяет выйти из строя, чтобы увидеть больше итераций вперед, потенциально помогая им поглощать недостатки кэша лучше. Это незначительная вещь, когда мы узлы в исполнительном блоке (в этом случае загрузите uops) даже без пропусков в кэше. Но с помощью hyperthreading вы получаете только каждый цикл полосы пропускания переднего плана, если другой поток не застопорился. Если он не слишком сильно конкурирует с нагрузкой и p0/1, меньшее количество флагов с объединенными доменами позволит этому циклу работать быстрее при совместном использовании ядра. (например, может быть, другой гиперпоток запускает много port5/port6 ​​и сохраняет uops?)

Так как un-lamination происходит после uop-cache, ваша версия не занимает дополнительного места в кэше uop. Disp32 с каждым uop в порядке и не занимает лишнего места. Но более объемный размер кода означает, что uop-cache с меньшей вероятностью будет паковать так же эффективно, так как вы попадете в границы 32B до того, как строки кэша uop будут заполнены чаще. (На самом деле, меньший код также не гарантирует лучшего. Меньшие инструкции могут привести к заполнению строки кэша uop и необходимости одной записи в другой строке до пересечения границы 32B.) Этот небольшой цикл может выполняться из буфера loopback (LSD), поэтому к счастью, uop-cache не является фактором.


Затем после цикла: Эффективная очистка - это трудная часть эффективной векторизации для небольших массивов, которая не может быть кратной коэффициенту unroll или особенно ширине вектора

    ...
    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-операнд очень хорош по сравнению с SSE, особенно при написании на C со встроенными функциями, поэтому вы не можете легко выбрать холодный регистр на movhlps в.