Почему 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 25

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 :

Есть 1.19 uops на петлю в Порту 5, это намного больше, чем ожидалось 0.5, не так ли о диспетчере uops, пытающемся сделать 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.