Почему Мулсс занимает всего 3 цикла в Haswell, в отличие от таблиц инструкций Агнера?

Я новичок в оптимизации инструкций.

Я сделал простой анализ простой функции dotp, которая используется для получения точечного произведения двух массивов с плавающей точкой.

Код C выглядит следующим образом:

float dotp(               
    const float  x[],   
    const float  y[],     
    const short  n      
)
{
    short i;
    float suma;
    suma = 0.0f;

    for(i=0; i<n; i++) 
    {    
        suma += x[i] * y[i];
    } 
    return suma;
}

Я использую тестовый фрейм, предоставленный Агнером Фогом на веб- тесте.

Массивы, которые используются в этом случае, выровнены:

int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);

float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;

Затем я вызываю функцию dotp, n=2048, repeat=100000:

 for (i = 0; i < repeat; i++)
 {
     sum = dotp(x,y,n);
 }

Я компилирую его с помощью gcc 4.8.3, с опцией компиляции -O3.

Я скомпилировал это приложение на компьютере, который не поддерживает инструкции FMA, поэтому вы можете видеть, что есть только инструкции SSE.

Код сборки:

.L13:
        movss   xmm1, DWORD PTR [rdi+rax*4]  
        mulss   xmm1, DWORD PTR [rsi+rax*4]   
        add     rax, 1                       
        cmp     cx, ax
        addss   xmm0, xmm1
        jg      .L13

Я делаю некоторый анализ:

          μops-fused  la    0    1    2    3    4    5    6    7    
movss       1          3             0.5  0.5
mulss       1          5   0.5  0.5  0.5  0.5
add         1          1   0.25 0.25               0.25   0.25 
cmp         1          1   0.25 0.25               0.25   0.25
addss       1          3         1              
jg          1          1                                   1                                                   -----------------------------------------------------------------------------
total       6          5    1    2     1     1      0.5   1.5

После запуска получаем результат:

   Clock  |  Core cyc |  Instruct |   BrTaken | uop p0   | uop p1      
--------------------------------------------------------------------
542177906 |609942404  |1230100389 |205000027  |261069369 |205511063 
--------------------------------------------------------------------  
   2.64   |  2.97     | 6.00      |     1     | 1.27     |  1.00   

   uop p2   |    uop p3   |  uop p4 |    uop p5  |  uop p6    |  uop p7       
-----------------------------------------------------------------------   
 205185258  |  205188997  | 100833  |  245370353 |  313581694 |  844  
-----------------------------------------------------------------------          
    1.00    |   1.00      | 0.00    |   1.19     |  1.52      |  0.00           

Вторая строка - это значение, считываемое из регистров Intel; третья строка делится на номер ветви "BrTaken".

Итак, мы видим, что в цикле есть 6 инструкций, 7 мопов, в соответствии с анализом.

Число мопов, запущенных на порт 0, порт 1, порт 5, порт 6, аналогично тому, что говорится в анализе. Я думаю, может быть, планировщик Uops делает это, он может попытаться сбалансировать нагрузки на порты, я прав?

Я абсолютно не понимаю, знаю, почему существует только около 3 циклов на цикл. Согласно таблице инструкций Агнера, задержка обучения mulss равно 5, и между циклами есть зависимости, так что, насколько я вижу, должно быть не менее 5 циклов на цикл.

Может ли кто-нибудь пролить некоторое понимание?

================================================== ================

Я попытался написать оптимизированную версию этой функции в nasm, развернув цикл в 8 раз и используя vfmadd231ps инструкция:

.L2:
    vmovaps         ymm1, [rdi+rax]             
    vfmadd231ps     ymm0, ymm1, [rsi+rax]       

    vmovaps         ymm2, [rdi+rax+32]          
    vfmadd231ps     ymm3, ymm2, [rsi+rax+32]    

    vmovaps         ymm4, [rdi+rax+64]          
    vfmadd231ps     ymm5, ymm4, [rsi+rax+64]    

    vmovaps         ymm6, [rdi+rax+96]          
    vfmadd231ps     ymm7, ymm6, [rsi+rax+96]   

    vmovaps         ymm8, [rdi+rax+128]         
    vfmadd231ps     ymm9, ymm8, [rsi+rax+128]  

    vmovaps         ymm10, [rdi+rax+160]               
    vfmadd231ps     ymm11, ymm10, [rsi+rax+160] 

    vmovaps         ymm12, [rdi+rax+192]                
    vfmadd231ps     ymm13, ymm12, [rsi+rax+192] 

    vmovaps         ymm14, [rdi+rax+224]                
    vfmadd231ps     ymm15, ymm14, [rsi+rax+224] 
    add             rax, 256                    
    jne             .L2

Результат:

  Clock   | Core cyc |  Instruct  |  BrTaken  |  uop p0   |   uop p1  
------------------------------------------------------------------------
 24371315 |  27477805|   59400061 |   3200001 |  14679543 |  11011601  
------------------------------------------------------------------------
    7.62  |     8.59 |  18.56     |     1     | 4.59      |     3.44


   uop p2  | uop p3  |  uop p4  |   uop p5  |   uop p6   |  uop p7  
-------------------------------------------------------------------------
 25960380  |26000252 |  47      |  537      |   3301043  |  10          
------------------------------------------------------------------------------
    8.11   |8.13     |  0.00    |   0.00    |   1.03     |  0.00        

Таким образом, мы можем видеть, что кэш данных L1 достигает 2*256 бит /8,59, он очень близок к пику 2*256/8, использование составляет около 93%, блок FMA используется только 8/8,59, пик составляет 2*8/8, использование составляет 47%.

Так что я думаю, что я достиг узкого места L1D, как ожидает Питер Кордес.

================================================== ================

Отдельное спасибо Boann, исправьте так много грамматических ошибок в моем вопросе.

================================================== ===============

Из ответа Питера я понял, что только регистр "чтения и записи" будет зависимостью, регистры "только для записи" не будут зависимостью.

Поэтому я пытаюсь уменьшить количество регистров, используемых в цикле, и пытаюсь развернуть их на 5, если все в порядке, я должен встретить то же самое узкое место, L1D.

.L2:
    vmovaps         ymm0, [rdi+rax]    
    vfmadd231ps     ymm1, ymm0, [rsi+rax]    

    vmovaps         ymm0, [rdi+rax+32]    
    vfmadd231ps     ymm2, ymm0, [rsi+rax+32]   

    vmovaps         ymm0, [rdi+rax+64]    
    vfmadd231ps     ymm3, ymm0, [rsi+rax+64]   

    vmovaps         ymm0, [rdi+rax+96]    
    vfmadd231ps     ymm4, ymm0, [rsi+rax+96]   

    vmovaps         ymm0, [rdi+rax+128]    
    vfmadd231ps     ymm5, ymm0, [rsi+rax+128]   

    add             rax, 160                    ;n = n+32
    jne             .L2 

Результат:

    Clock  | Core cyc  | Instruct  |  BrTaken |    uop p0  |   uop p1  
------------------------------------------------------------------------  
  25332590 |  28547345 |  63700051 |  5100001 |   14951738 |  10549694   
------------------------------------------------------------------------
    4.97   |  5.60     | 12.49     |    1     |     2.93   |    2.07    

    uop p2  |uop p3   | uop p4 | uop p5 |uop p6   |  uop p7 
------------------------------------------------------------------------------  
  25900132  |25900132 |   50   |  683   | 5400909 |     9  
-------------------------------------------------------------------------------     
    5.08    |5.08     |  0.00  |  0.00  |1.06     |     0.00    

Мы видим 5/5,60 = 89,45%, это немного меньше, чем уроллинг на 8, что-то не так?

================================================== ===============

Я пытаюсь развернуть цикл на 6, 7 и 15, чтобы увидеть результат. Я также снова разверну на 5 и 8, чтобы дважды подтвердить результат.

Результат следующий: мы видим, что на этот раз результат намного лучше, чем раньше.

Хотя результат нестабилен, коэффициент развертывания больше, а результат лучше.

            | L1D bandwidth     |  CodeMiss | L1D Miss | L2 Miss 
----------------------------------------------------------------------------
  unroll5   | 91.86% ~ 91.94%   |   3~33    | 272~888  | 17~223
--------------------------------------------------------------------------
  unroll6   | 92.93% ~ 93.00%   |   4~30    | 481~1432 | 26~213
--------------------------------------------------------------------------
  unroll7   | 92.29% ~ 92.65%   |   5~28    | 336~1736 | 14~257
--------------------------------------------------------------------------
  unroll8   | 95.10% ~ 97.68%   |   4~23    | 363~780  | 42~132
--------------------------------------------------------------------------
  unroll15  | 97.95% ~ 98.16%   |   5~28    | 651~1295 | 29~68

================================================== ===================

Я пытаюсь скомпилировать функцию с gcc 7.1 в сети " https://gcc.godbolt.org/"

Опция компиляции - "-O3 -march=haswell -mtune=intel", аналогичная gcc 4.8.3.

.L3:
        vmovss  xmm1, DWORD PTR [rdi+rax]
        vfmadd231ss     xmm0, xmm1, DWORD PTR [rsi+rax]
        add     rax, 4
        cmp     rdx, rax
        jne     .L3
        ret

1 ответ

Решение

Посмотрите на вашу петлю еще раз: movss xmm1, src не зависит от старого значения xmm1 потому что его назначение только для записи. Каждая итерация mulss является независимым Внеочередное выполнение может использовать и использует этот параллелизм на уровне команд, так что вы определенно не ограничиваетесь mulss задержка.

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

Переименование регистров с помощью алгоритма Томасуло убирает все, кроме фактических истинных зависимостей (чтение после записи), поэтому любая инструкция, в которой пункт назначения не является также регистром-источником, не имеет взаимодействия с цепочкой зависимостей, включающей старое значение этого регистра. (За исключением ложных зависимостей, таких как popcnt на процессорах Intel, и запись только части регистра без очистки остальных (как mov al, 5 или же sqrtss xmm2, xmm1). Связано: почему большинство команд x64 обнуляют верхнюю часть 32-битного регистра).


Вернуться к вашему коду:

.L13:
    movss   xmm1, DWORD PTR [rdi+rax*4]  
    mulss   xmm1, DWORD PTR [rsi+rax*4]   
    add     rax, 1                       
    cmp     cx, ax
    addss   xmm0, xmm1
    jg      .L13

Зависимости, переносимые циклом (от одной итерации к следующей), каждая:

  • xmm0 читать и писать addss xmm0, xmm1, который имеет 3 цикла задержки на Haswell.
  • rax читать и писать add rax, 1, 1с задержка, так что это не критический путь.

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

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

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


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

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

Я не уверен, почему у вас есть * в вашем столе на cmp а также add инструкции. (обновление: я просто догадывался, что вы использовали нотацию, как это делает IACA, но, видимо, это не так). Ни один из них не перегорел. Единственное слияние происходит микро-слияние mulss xmm1, [rsi+rax*4],

А поскольку это инструкция ALU с двумя операндами с регистром назначения чтения-изменения-записи, она остается слитой в макросе даже в ROB на Haswell. (Sandybridge разложил бы его во время выпуска.) Обратите внимание, что vmulss xmm1, xmm1, [rsi+rax*4] разложил бы ламинат на Haswell, также.

Ничто из этого на самом деле не имеет значения, поскольку вы просто полностью ограничиваете задержку добавления FP, намного медленнее, чем любые ограничения пропускной способности. Без -ffast-math, компиляторы ничего не могут сделать. С -ffast-math, clang обычно разворачивается с несколькими аккумуляторами и автоматически векторизуется, поэтому они будут векторными аккумуляторами. Таким образом, вы можете, вероятно, насытить предел пропускной способности Haswell в 1 векторное или скалярное прибавление FP за такт, если попадете в кэш L1D.

С FMA с задержкой 5c и пропускной способностью 0.5c на Haswell вам потребуется 10 аккумуляторов, чтобы поддерживать 10 FMA в полете и максимально увеличивать пропускную способность FMA, поддерживая насыщение p0/p 1 FMA. (Skylake уменьшил задержку FMA до 4 циклов и выполняет умножение, сложение и FMA на устройствах FMA. Таким образом, у него на самом деле задержка при добавлении выше, чем у Haswell.)

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


Re: моп на порт:

в порту 5 имеется 1,19 мопов на цикл, это намного больше, чем ожидалось 0,5, это вопрос о том, что диспетчер мопов пытается сделать мопы на каждом порту одинаковыми

Да что-то подобное.

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

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

Taken-ветки могут работать только на порту 6. Порт 5 не имеет никаких мопов в этом цикле, которые могут работать только там, поэтому он в конечном итоге привлекает много мопов с многими портами.

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

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

Связанные / более подробная информация: Как точно запланировано выполнение x86-мопов?


Больше материала для анализа производительности:

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

  • цепочки зависимостей (как в этом случае)
  • внешняя пропускная способность (максимум 4 Uops из слитого домена выдается за такт на Haswell)
  • узкие места порта выполнения, например, если многим мопам нужен p0/p 1 или p2/p3, как в вашем развернутом цикле. Подсчитайте число мопов в домене без использования для определенных портов. Как правило, вы можете предполагать распределение в лучшем случае, когда мопы могут запускаться на других портах, не украдя занятые порты очень часто, но это действительно случается.

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

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

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

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


Настройка вашего цикла FMA:

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

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

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

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

Наше узкое место в нагрузке означает, что пропускная способность FMA в лучшем случае составляет половину от максимальной, поэтому нам нужно как минимум 5 векторных аккумуляторов, чтобы скрыть их задержку. 8 хорошо, так что в цепочках зависимостей есть много провалов, чтобы они могли наверстать упущенное после любых задержек из-за неожиданной задержки или конкуренции за p0/p 1. 7 или, может быть, даже 6 тоже подойдут: ваш фактор разворота не должен быть степенью 2.

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

Если вы оставите некоторую слабость в цепочках зависимостей, выполнение вне порядка может "догнать" FMA, поскольку они не будут узкими местами по пропускной способности или задержке, просто ожидая результатов загрузки. @Forward обнаружил (в обновлении вопроса), что развертывание на 5 снижает производительность с 93% пропускной способности L1D до 89,5% для этого цикла.

Я предполагаю, что развертка на 6 (на единицу больше, чем минимум, чтобы скрыть задержку) будет в порядке, и будет примерно такая же производительность, как и развертка на 8. Если бы мы были ближе к максимальному увеличению пропускной способности FMA (а не просто к узким местам при загрузке) пропускной способности), одного больше минимального может быть недостаточно.

обновление: экспериментальный тест @Forward показывает, что мое предположение было неверным. Нет большой разницы между unroll5 и unroll6. Кроме того, unroll15 вдвое ближе, чем unroll8, к теоретической максимальной пропускной способности 2x 256b нагрузок на такт. Измерение только с независимыми нагрузками в контуре или с независимыми нагрузками и FMA только для регистров, скажет нам, сколько из этого происходит из-за взаимодействия с цепочкой зависимостей FMA. Даже в лучшем случае не получится идеальная пропускная способность 100%, хотя бы из-за ошибок измерения и сбоев из-за прерываний таймера. (Linux perf измеряет только циклы пользовательского пространства, если вы не запускаете его как root, но время по-прежнему включает время, потраченное на обработчики прерываний. Вот почему частота вашего процессора может быть сообщена как 3,87 ГГц при работе от имени пользователя root, но 3,900 ГГц при запуске от имени пользователя root и измерения. cycles вместо cycles:u.)


Мы не ограничиваемся пропускной способностью внешнего интерфейса, но мы можем уменьшить количество мопов в слитых доменах, избегая индексированных режимов адресации для mov инструкции. Чем меньше, тем лучше и делает это более дружественным к гиперпоточности, когда делится ядром с чем-то другим, кроме этого.

Простой способ - просто сделать два приращения указателя внутри цикла. Сложный способ - это аккуратный способ индексации одного массива относительно другого:

;; input pointers for x[] and y[] in rdi and rsi
;; size_t n  in rdx

    ;;; zero ymm1..8, or load+vmulps into them

    add             rdx, rsi             ; end_y
    ; lea rdx, [rdx+rsi-252]  to break out of the unrolled loop before going off the end, with odd n

    sub             rdi, rsi             ; index x[] relative to y[], saving one pointer increment

.unroll8:
    vmovaps         ymm0, [rdi+rsi]            ; *px, actually py[xy_offset]
    vfmadd231ps     ymm1, ymm0, [rsi]          ; *py

    vmovaps         ymm0,       [rdi+rsi+32]   ; write-only reuse of ymm0
    vfmadd231ps     ymm2, ymm0, [rsi+32]

    vmovaps         ymm0,       [rdi+rsi+64]
    vfmadd231ps     ymm3, ymm0, [rsi+64]

    vmovaps         ymm0,       [rdi+rsi+96]
    vfmadd231ps     ymm4, ymm0, [rsi+96]

    add             rsi, 256       ; pointer-increment here
                                   ; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
                                   ; smaller code-size helps in the big picture, but not for a micro-benchmark

    vmovaps         ymm0,       [rdi+rsi+128-256]  ; be pedantic in the source about compensating for the pointer-increment
    vfmadd231ps     ymm5, ymm0, [rsi+128-256]
    vmovaps         ymm0,       [rdi+rsi+160-256]
    vfmadd231ps     ymm6, ymm0, [rsi+160-256]
    vmovaps         ymm0,       [rdi+rsi-64]       ; or not
    vfmadd231ps     ymm7, ymm0, [rsi-64]
    vmovaps         ymm0,       [rdi+rsi-32]
    vfmadd231ps     ymm8, ymm0, [rsi-32]

    cmp             rsi, rdx
    jb              .unroll8                 ; } while(py < endy);

Использование неиндексированного режима адресации в качестве операнда памяти для vfmaddps позволяет ему оставаться в микроплавленом состоянии в ядре, вышедшем из строя, вместо того, чтобы быть неслоистым. Режимы микросинтеза и адресации

Так что мой цикл - это 18 мопов слитых доменов для 8 векторов. Yours берет 3 uops из fused-domain для каждой пары vmovaps + vfmaddps вместо 2 из-за отсутствия ламинирования индексированных режимов адресации. У обоих из них, конечно, по-прежнему есть 2 загрузки в неиспользованном домене (port2/3) на пару, так что это все еще узкое место.

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

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


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

    ...
    jb

    ;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
    ;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.

    ; reduce down to a single vector, with a tree of dependencies
    vaddps          ymm1, ymm2, ymm1
    vaddps          ymm3, ymm4, ymm3
    vaddps          ymm5, ymm6, ymm5
    vaddps          ymm7, ymm8, ymm7

    vaddps          ymm0, ymm3, ymm1
    vaddps          ymm1, ymm7, ymm5

    vaddps          ymm0, ymm1, ymm0

    ; horizontal within that vector, low_half += high_half until we're down to 1
    vextractf128    xmm1, ymm0, 1
    vaddps          xmm0, xmm0, xmm1
    vmovhlps        xmm1, xmm0, xmm0        
    vaddps          xmm0, xmm0, xmm1
    vmovshdup       xmm1, xmm0
    vaddss          xmm0, xmm1
    ; this is faster than 2x vhaddps

    vzeroupper    ; important if returning to non-AVX-aware code after using ymm regs.
    ret           ; with the scalar result in xmm0

Для получения дополнительной информации о горизонтальной сумме в конце см. Самый быстрый способ сделать горизонтальную векторную сумму с плавающей точкой на x86. Два 128-битных шаффла, которые я использовал, даже не нуждаются в непосредственном байте управления, поэтому он экономит 2 байта размера кода по сравнению с более очевидным shufps, (И 4 байта размера кода против vpermilps, потому что этот код операции всегда нуждается в 3-байтовом префиксе VEX, а также немедленном). AVX с 3 операндами очень хорош по сравнению с SSE, особенно при написании на C со встроенными функциями, поэтому вы не можете так просто выбрать холодный регистр для movhlps в.

Другие вопросы по тегам