Ширина полосы пропускания L1: снижение эффективности на 50% с использованием адресов, которые отличаются на 4096 + 64 байта

Я хочу добиться максимальной пропускной способности следующих операций с процессорами Intel.

for(int i=0; i<n; i++) z[i] = x[i] + y[i]; //n=2048

где x, y и z - массивы с плавающей точкой. Я делаю это на системах Хасуэлла, Айви-Бридж и Уэстмира.

Я изначально выделял такую ​​память

char *a = (char*)_mm_malloc(sizeof(float)*n, 64);
char *b = (char*)_mm_malloc(sizeof(float)*n, 64);
char *c = (char*)_mm_malloc(sizeof(float)*n, 64);
float *x = (float*)a; float *y = (float*)b; float *z = (float*)c;

Когда я это сделал, я получил около 50% максимальной пропускной способности, ожидаемой для каждой системы.

Пиковые значения рассчитываются как frequency * average bytes/clock_cycle. Средний байт/тактовый цикл для каждой системы:

Core2: two 16 byte reads one 16 byte write per 2 clock cycles     -> 24 bytes/clock cycle
SB/IB: two 32 byte reads and one 32 byte write per 2 clock cycles -> 48 bytes/clock cycle
Haswell: two 32 byte reads and one 32 byte write per clock cycle  -> 96 bytes/clock cycle

Это означает, что, например, на Haswell я Я наблюдаю только 48 байтов/такт (может быть два чтения за один такт и один записать следующий такт).

Я распечатал разницу в адресе b-a и c-b и каждый из них - 8256 байт. Значение 8256 равно 8192 + 64. Таким образом, каждый из них больше размера массива (8192 байта) одной строкой кэша.

По прихоти я попытался выделить такую ​​память.

const int k = 0;
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float)+k*64;
char *c = b+n*sizeof(float)+k*64;
float *x = (float*)a; float *y = (float*)b; float *z = (float*)c;

Это почти удвоило мою максимальную пропускную способность, так что теперь я получаю около 90% максимальной пропускной способности. Однако, когда я пробовал k=1, он упал до 50%. Я пробовал другие значения k и обнаружил, что, например, k=2, k=33, k=65 получает только 50% пика, но, например, k=10, k=32, k=63 дали полную скорость. Я не понимаю этого.

В руководстве по микроархитектуре Agner Fog он говорит, что существует ложная зависимость с адресом памяти с тем же набором и смещением

Невозможно читать и писать одновременно с адресов которые разнесены на 4 Кбайта.

Но именно там, где я вижу самую большую выгоду! Когда k=0 адрес памяти отличается точно 2*4096 байтами. Агнер также рассказывает о конфликтах банка-кэша. Но Хасуэлл и Уэстмир не предполагают иметь эти банковские конфликты, чтобы не объяснять, что я наблюдаю. Что происходит!?

Я понимаю, что исполнение OoO решает, какой адрес читать и писать так, даже если адреса памяти массивов отличаются примерно на 4096 байт, что не обязательно означает, что процессор читает, например. &x[0] и записывает &z[0] одновременно, но тогда почему бы отключить одну строку кэша, чтобы он задохнулся?

Редактировать: На основании ответа Евгения Клюева я теперь верю, что это то, что Агнер Фог называет "фальшивым магазином переадресации". В своем руководстве под Pentium Pro, II и II он пишет:

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

; Example 5.28. Bogus store-to-load forwarding stall
mov byte ptr [esi], al
mov ebx, dword ptr [esi+4092]
; No stall
mov ecx, dword ptr [esi+4096]
; Bogus stall

Изменить: Здесь приведена таблица эффективности каждой системы для k=0 и k=1.

               k=0      k=1        
Westmere:      99%      66%
Ivy Bridge:    98%      44%
Haswell:       90%      49%

Я думаю, что могу объяснить эти числа, если предположить, что для k=1, что записи и чтения не могут произойти в одном такте.

       cycle     Westmere          Ivy Bridge           Haswell
           1     read  16          read  16 read  16    read  32 read 32
           2     write 16          read  16 read  16    write 32
           3                       write 16
           4                       write 16  

k=1/k=0 peak    16/24=66%          24/48=50%            48/96=50%

Эта теория работает очень хорошо. Мост Ivy немного ниже, чем я ожидал, но Ivy Bridge страдает от конфликтов банковского кэша, когда другие не делают этого, что может быть другим эффектом для рассмотрения.

Ниже приведен рабочий код, чтобы проверить это самостоятельно. В системе без компиляции AVX с g++ -O3 sum.cpp иначе скомпилируйте с помощью g++ -O3 -mavx sum.cpp. Попробуйте изменить значение k.

//sum.cpp
#include <x86intrin.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define TIMER_TYPE CLOCK_REALTIME

double time_diff(timespec start, timespec end)
{
    timespec temp;
    if ((end.tv_nsec-start.tv_nsec)<0) {
        temp.tv_sec = end.tv_sec-start.tv_sec-1;
        temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
    } else {
        temp.tv_sec = end.tv_sec-start.tv_sec;
        temp.tv_nsec = end.tv_nsec-start.tv_nsec;
    }
    return (double)temp.tv_sec +  (double)temp.tv_nsec*1E-9;
}

void sum(float * __restrict x, float * __restrict y, float * __restrict z, const int n) {
    #if defined(__GNUC__)
    x = (float*)__builtin_assume_aligned (x, 64);
    y = (float*)__builtin_assume_aligned (y, 64);
    z = (float*)__builtin_assume_aligned (z, 64);
    #endif
    for(int i=0; i<n; i++) {
        z[i] = x[i] + y[i];
    }
}

#if (defined(__AVX__))
void sum_avx(float *x, float *y, float *z, const int n) {
    float *x1 = x;
    float *y1 = y;
    float *z1 = z;
    for(int i=0; i<n/64; i++) { //unroll eight times
        _mm256_store_ps(z1+64*i+  0,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 0), _mm256_load_ps(y1+64*i+  0)));
        _mm256_store_ps(z1+64*i+  8,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 8), _mm256_load_ps(y1+64*i+  8)));
        _mm256_store_ps(z1+64*i+ 16,_mm256_add_ps(_mm256_load_ps(x1+64*i+16), _mm256_load_ps(y1+64*i+ 16)));
        _mm256_store_ps(z1+64*i+ 24,_mm256_add_ps(_mm256_load_ps(x1+64*i+24), _mm256_load_ps(y1+64*i+ 24)));
        _mm256_store_ps(z1+64*i+ 32,_mm256_add_ps(_mm256_load_ps(x1+64*i+32), _mm256_load_ps(y1+64*i+ 32)));
        _mm256_store_ps(z1+64*i+ 40,_mm256_add_ps(_mm256_load_ps(x1+64*i+40), _mm256_load_ps(y1+64*i+ 40)));
        _mm256_store_ps(z1+64*i+ 48,_mm256_add_ps(_mm256_load_ps(x1+64*i+48), _mm256_load_ps(y1+64*i+ 48)));
        _mm256_store_ps(z1+64*i+ 56,_mm256_add_ps(_mm256_load_ps(x1+64*i+56), _mm256_load_ps(y1+64*i+ 56)));
    }
}
#else
void sum_sse(float *x, float *y, float *z, const int n) {
    float *x1 = x;
    float *y1 = y;
    float *z1 = z;
    for(int i=0; i<n/32; i++) { //unroll eight times
        _mm_store_ps(z1+32*i+  0,_mm_add_ps(_mm_load_ps(x1+32*i+ 0), _mm_load_ps(y1+32*i+  0)));
        _mm_store_ps(z1+32*i+  4,_mm_add_ps(_mm_load_ps(x1+32*i+ 4), _mm_load_ps(y1+32*i+  4)));
        _mm_store_ps(z1+32*i+  8,_mm_add_ps(_mm_load_ps(x1+32*i+ 8), _mm_load_ps(y1+32*i+  8)));
        _mm_store_ps(z1+32*i+ 12,_mm_add_ps(_mm_load_ps(x1+32*i+12), _mm_load_ps(y1+32*i+ 12)));
        _mm_store_ps(z1+32*i+ 16,_mm_add_ps(_mm_load_ps(x1+32*i+16), _mm_load_ps(y1+32*i+ 16)));
        _mm_store_ps(z1+32*i+ 20,_mm_add_ps(_mm_load_ps(x1+32*i+20), _mm_load_ps(y1+32*i+ 20)));
        _mm_store_ps(z1+32*i+ 24,_mm_add_ps(_mm_load_ps(x1+32*i+24), _mm_load_ps(y1+32*i+ 24)));
        _mm_store_ps(z1+32*i+ 28,_mm_add_ps(_mm_load_ps(x1+32*i+28), _mm_load_ps(y1+32*i+ 28)));
    }
}
#endif

int main () {
    const int n = 2048;
    const int k = 0;
    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)+k*64;
    char *c = b+n*sizeof(float)+k*64;

    float *x = (float*)a;
    float *y = (float*)b;
    float *z = (float*)c;
    printf("x %p, y %p, z %p, y-x %d, z-y %d\n", a, b, c, b-a, c-b);

    for(int i=0; i<n; i++) {
        x[i] = (1.0f*i+1.0f);
        y[i] = (1.0f*i+1.0f);
        z[i] = 0;
    }
    int repeat = 1000000;
    timespec time1, time2;

    sum(x,y,z,n);
    #if (defined(__AVX__))
    sum_avx(x,y,z2,n);
    #else
    sum_sse(x,y,z2,n);
    #endif
    printf("error: %d\n", memcmp(z,z2,sizeof(float)*n));

    while(1) {
        clock_gettime(TIMER_TYPE, &time1);
        #if (defined(__AVX__))
        for(int r=0; r<repeat; r++) sum_avx(x,y,z,n);
        #else
        for(int r=0; r<repeat; r++) sum_sse(x,y,z,n);
        #endif
        clock_gettime(TIMER_TYPE, &time2);

        double dtime = time_diff(time1,time2);
        double peak = 1.3*96; //haswell @1.3GHz
        //double peak = 3.6*48; //Ivy Bridge @ 3.6Ghz
        //double peak = 2.4*24; // Westmere @ 2.4GHz
        double rate = 3.0*1E-9*sizeof(float)*n*repeat/dtime;
        printf("dtime %f, %f GB/s, peak, %f, efficiency %f%%\n", dtime, rate, peak, 100*rate/peak);
    }
}

Ответ 1

Я думаю, что разрыв между a и b не имеет значения. После оставления только одного промежутка между b и c у меня есть следующие результаты по Haswell:

k   %
-----
1  48
2  48
3  48
4  48
5  46
6  53
7  59
8  67
9  73
10 81
11 85
12 87
13 87
...
0  86

Так как Хасуэлл, как известно, свободен от банковских конфликтов, единственным оставшимся объяснением является ложная зависимость между адресами памяти (и вы нашли подходящее место в руководстве по микроархитектуре Agner Fog, объясняющем именно эту проблему). Разница между банковским конфликтом и ложным обменом заключается в том, что конфликт в банке предотвращает одновременный доступ к одному банку в течение того же тактового цикла, в то время как ложный обмен предотвращает чтение с некоторого смещения в 4 КБ памяти сразу после того, как вы написали что-то на такое же смещение (и не только в течение того же такта, но также и для нескольких тактов после записи).

Так как ваш код (для k=0) записывает на любое смещение только после, делая два чтения с одного и того же смещения и не будет читать из него в течение очень долгого времени, этот случай следует рассматривать как "best", поэтому я положил k=0 в конец таблицы. Для k=1 вы всегда читаете смещение, которое совсем недавно перезаписывается, что означает ложное совместное использование и, следовательно, ухудшение производительности. При увеличении k время между чтением и чтением увеличивается, и ядро ​​ЦП имеет больше шансов передать письменные данные через всю иерархию памяти (что означает два перевода адреса для чтения и записи, обновление данных кеша и тегов и получение данных из кеша, синхронизацию данных между ядра и, возможно, еще много вещей). k=12 или 24 часа (на моем процессоре) достаточно, чтобы каждый письменный кусок данных был готов для последующих операций чтения, поэтому начиная с этого значения производительность возвращается к обычному. Выглядит не очень сильно отличается от 20 + часов на AMD (как сказал @Mysticial).

Ответ 2

TL; DR: при определенных значениях k возникает слишком много условий наложения псевдонимов 4K, что является основной причиной ухудшения пропускной способности. В псевдонимах 4K нагрузка останавливается без необходимости, тем самым увеличивая эффективную задержку загрузки и останавливая все последующие зависимые инструкции. Это, в свою очередь, приводит к уменьшению использования полосы пропускания L1. Для этих значений k большинство условий наложения 4K могут быть устранены путем разделения цикла следующим образом:

for(int i=0; i<n/64; i++) {
    _mm256_store_ps(z1+64*i+  0,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 0), _mm256_load_ps(y1+64*i+  0)));
    _mm256_store_ps(z1+64*i+  8,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 8), _mm256_load_ps(y1+64*i+  8)));
}
for(int i=0; i<n/64; i++) {
    _mm256_store_ps(z1+64*i+ 16,_mm256_add_ps(_mm256_load_ps(x1+64*i+16), _mm256_load_ps(y1+64*i+ 16)));
    _mm256_store_ps(z1+64*i+ 24,_mm256_add_ps(_mm256_load_ps(x1+64*i+24), _mm256_load_ps(y1+64*i+ 24)));
}
for(int i=0; i<n/64; i++) {
    _mm256_store_ps(z1+64*i+ 32,_mm256_add_ps(_mm256_load_ps(x1+64*i+32), _mm256_load_ps(y1+64*i+ 32)));
    _mm256_store_ps(z1+64*i+ 40,_mm256_add_ps(_mm256_load_ps(x1+64*i+40), _mm256_load_ps(y1+64*i+ 40)));
}
for(int i=0; i<n/64; i++) {
    _mm256_store_ps(z1+64*i+ 48,_mm256_add_ps(_mm256_load_ps(x1+64*i+48), _mm256_load_ps(y1+64*i+ 48)));
    _mm256_store_ps(z1+64*i+ 56,_mm256_add_ps(_mm256_load_ps(x1+64*i+56), _mm256_load_ps(y1+64*i+ 56)));
}

Это разделение устраняет большинство 4K-псевдонимов для случаев, когда k является нечетным положительным целым числом (например, 1). Достигнутая пропускная способность L1 улучшена примерно на 50% в Haswell. Есть еще возможности для совершенствования, например, путем развертывания цикла и поиска способа не использовать режим индексированной адресации для загрузок и хранилищ.

Однако это разделение не устраняет псевдонимы 4K для четных значений k. Поэтому для четных значений k необходимо использовать другое разбиение. Однако, когда k равно 0, оптимальная производительность может быть достигнута без разделения цикла. В этом случае производительность привязывается к портам 1, 2, 3, 4 и 7 одновременно.

В некоторых случаях при одновременной загрузке и сохранении может быть штраф в несколько циклов, но в данном конкретном случае это наказание в основном не существует, поскольку в принципе таких конфликтов нет (т.е. Адреса одновременных загрузок). и магазины достаточно далеко друг от друга). Кроме того, общий размер рабочего набора вписывается в L1, поэтому нет трафика L1-L2 за пределами первого выполнения цикла.

Остальная часть этого ответа включает в себя подробное объяснение этого резюме.


Во-первых, обратите внимание, что три массива имеют общий размер 24 КБ. Кроме того, поскольку вы инициализируете массивы перед выполнением основного цикла, большинство обращений в основном цикле попадет в L1D, размер которого составляет 32 КБ, и 8-полосный ассоциативный на современных процессорах Intel. Поэтому нам не нужно беспокоиться о промахах или аппаратной предварительной загрузке. Наиболее важным событием производительности в этом случае является LD_BLOCKS_PARTIAL.ADDRESS_ALIAS, которое происходит, когда частичное сравнение адресов, включающее более позднюю загрузку, приводит к совпадению с более ранним хранилищем, и все условия пересылки хранилища удовлетворяются, но целевые местоположения фактически отличаются, Intel называет эту ситуацию псевдонимом 4K или ложной пересылкой хранилища. Наблюдаемое снижение производительности при использовании псевдонимов 4K зависит от окружающего кода.

Измеряя cycles, LD_BLOCKS_PARTIAL.ADDRESS_ALIAS и MEM_UOPS_RETIRED.ALL_LOADS, мы видим, что для всех значений k где достигнутая пропускная способность намного меньше, чем пиковая пропускная способность, LD_BLOCKS_PARTIAL.ADDRESS_ALIAS и MEM_UOPS_RETIRED.ALL_LOADS почти равны. Также для всех значений k где достигнутая пропускная способность близка к пиковой пропускной способности, LD_BLOCKS_PARTIAL.ADDRESS_ALIAS очень мала по сравнению с MEM_UOPS_RETIRED.ALL_LOADS. Это подтверждает, что происходит снижение полосы пропускания из-за большинства нагрузок, страдающих от псевдонимов 4K.

В разделе 12.8 руководства по оптимизации Intel говорится следующее:

Псевдоним памяти размером 4 КБ возникает, когда код сохраняется в одной области памяти, а вскоре после этого он загружается из другой области памяти со смещением в 4 КБ между ними. Например, загрузка по линейному адресу 0x400020 следует за магазином по линейному адресу 0x401020.

Загрузка и сохранение имеют одно и то же значение для битов 5–11 их адресов, а смещенные байты должны иметь частичное или полное перекрытие.

То есть есть два необходимых условия для последующей загрузки в псевдоним с более ранним хранилищем:

  • Биты 5-11 двух линейных адресов должны быть равны.
  • Места, к которым осуществляется доступ, должны перекрываться (поэтому могут быть некоторые данные для пересылки).

На процессорах, которые поддерживают AVX-512, мне кажется, что один загрузочный моп может загрузить до 64 байт. Поэтому я думаю, что диапазон для первого условия должен быть 6-11 вместо 5-11.

В следующем листинге показана (32-байтовая) последовательность обращений к памяти на основе AVX и 12 младших разрядов их адресов для двух разных значений k.

======
k=0
======
load x+(0*64+0)*4  = x+0 where x is 4k aligned    0000 000|0 0000
load y+(0*64+0)*4  = y+0 where y is 4k aligned    0000 000|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k aligned    0000 000|0 0000
load x+(0*64+8)*4  = x+32 where x is 4k aligned   0000 001|0 0000
load y+(0*64+8)*4  = y+32 where y is 4k aligned   0000 001|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k aligned   0000 001|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned   0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k aligned   0000 010|0 0000
store z+(0*64+16)*4= z+64 where z is 4k aligned   0000 010|0 0000
load x+(0*64+24)*4  = x+96 where x is 4k aligned  0000 011|0 0000
load y+(0*64+24)*4  = y+96 where y is 4k aligned  0000 011|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k aligned  0000 011|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned  0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k aligned  0000 100|0 0000
store z+(0*64+32)*4= z+128 where z is 4k aligned  0000 100|0 0000
.
.
.
======
k=1
======
load x+(0*64+0)*4  = x+0 where x is 4k aligned       0000 000|0 0000
load y+(0*64+0)*4  = y+0 where y is 4k+64 aligned    0000 010|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k+128 aligned   0000 100|0 0000
load x+(0*64+8)*4  = x+32 where x is 4k aligned      0000 001|0 0000
load y+(0*64+8)*4  = y+32 where y is 4k+64 aligned   0000 011|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k+128 aligned  0000 101|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned      0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k+64 aligned   0000 100|0 0000
store z+(0*64+16)*4= z+64 where z is 4k+128 aligned  0000 110|0 0000
load x+(0*64+24)*4  = x+96 where x is 4k aligned     0000 011|0 0000
load y+(0*64+24)*4  = y+96 where y is 4k+64 aligned  0000 101|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k+128 aligned 0000 111|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned     0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k+64 aligned  0000 110|0 0000
store z+(0*64+32)*4= z+128 where z is 4k+128 aligned 0001 000|0 0000
.
.
.

Обратите внимание, что когда k = 0, нет нагрузки, по-видимому, удовлетворяют двум условиям наложения 4K. С другой стороны, когда k = 1, все нагрузки кажутся удовлетворяющими условиям. Однако это утомительно делать вручную для всех итераций и всех значений k. Итак, я написал программу, которая в основном генерирует адреса обращений к памяти и вычисляет общее количество нагрузок, которые претерпели псевдонимы 4K для различных значений k. Одна проблема, с которой я столкнулся, заключалась в том, что мы не знаем, для какой-либо данной загрузки, количество хранилищ, которые все еще находятся в буфере хранилищ (еще не были зафиксированы). Поэтому я разработал симулятор так, чтобы он мог использовать разные пропускные способности магазина для разных значений k, что, кажется, лучше отражает то, что на самом деле происходит на реальном процессоре. Код можно найти здесь.

На следующем рисунке показано количество случаев наложения 4K, созданных симулятором, по сравнению с измеренным числом с использованием LD_BLOCKS_PARTIAL.ADDRESS_ALIAS в Haswell. Я настроил пропускную способность магазина, используемую в симуляторе, для каждого значения k чтобы сделать две кривые максимально похожими. На втором рисунке показана обратная пропускная способность магазина (общее количество циклов, деленное на общее количество магазинов), использованная в симуляторе и измеренная в Haswell. Обратите внимание, что пропускная способность магазина при k = 0 не имеет значения, потому что в любом случае нет псевдонимов 4K. Поскольку для каждого хранилища имеется две загрузки, пропускная способность обратной загрузки составляет половину пропускной способности обратного хранилища.

enter image description here

enter image description here

Очевидно, что время, которое каждый магазин остается в буфере магазина, отличается на Haswell и симуляторе, поэтому мне нужно было использовать разные пропускные способности, чтобы сделать две кривые похожими. Симулятор можно использовать, чтобы показать, как пропускная способность магазина может повлиять на количество псевдонимов 4K. Если пропускная способность магазина очень близка к 1c/store, то количество случаев с псевдонимами 4K было бы намного меньше. Условия псевдонимов 4K не приводят к сбросам конвейера, но могут привести к повторным попыткам передачи с RS. В данном конкретном случае я не наблюдал никаких повторов.

Я думаю, что смогу объяснить эти числа, если предположу, что при k = 1 запись и чтение не могут происходить в одном и том же тактовом цикле.

На самом деле существует штраф в несколько циклов при одновременной загрузке и сохранении, но они могут происходить только тогда, когда адреса загрузки и сохранения находятся в пределах 64 байтов (но не равны) в Haswell или 32 байтов в Ivy Bridge. и песчаный мост. Странные эффекты производительности от соседних зависимых хранилищ в цикле погони за указателями на IvyBridge. Добавление дополнительной нагрузки ускоряет это? , В этом случае адреса всех обращений выровнены по 32 байта, но на IvB все порты L1 имеют размер 16 байт, поэтому на Haswell и IvB может быть наложен штраф. На самом деле, поскольку загрузка и сохранение могут занять больше времени для удаления, а буферов загрузки больше, чем буферов хранения, более вероятно, что при более поздней загрузке будет ложно-псевдоним более раннего хранилища. Однако возникает вопрос о том, как штраф за псевдоним 4K и штраф за доступ L1 взаимодействуют друг с другом и влияют на общую производительность. Используя событие CYCLE_ACTIVITY.STALLS_LDM_PENDING и средство мониторинга производительности задержки загрузки MEM_TRANS_RETIRED.LOAD_LATENCY_GT_*, мне кажется, что не существует наблюдаемого штрафа за доступ к L1. Это означает, что в большинстве случаев адреса одновременных загрузок и хранилищ не налагают штраф. Следовательно, штраф за наложение 4K является основной причиной ухудшения пропускной способности.

Я использовал следующий код для проведения измерений на Haswell. По сути, это тот же код, который испускается g++ -O3 -mavx.

%define SIZE 64*64*2
%define K_   10

BITS 64
DEFAULT REL

GLOBAL main

EXTERN printf
EXTERN exit

section .data
align 4096
bufsrc1: times (SIZE+(64*K_)) db 1
bufsrc2: times (SIZE+(64*K_)) db 1
bufdest: times SIZE db 1

section .text
global _start
_start:
    mov rax, 1000000

.outer:
    mov rbp, SIZE/256
    lea rsi, [bufsrc1]
    lea rdi, [bufsrc2]
    lea r13, [bufdest]

.loop:
    vmovaps ymm1, [rsi]
    vaddps  ymm0, ymm1, [rdi]

    add rsi, 256
    add rdi, 256
    add r13, 256

    vmovaps[r13-256], ymm0

    vmovaps  ymm2, [rsi-224]
    vaddps   ymm0, ymm2, [rdi-224]
    vmovaps  [r13-224], ymm0

    vmovaps  ymm3, [rsi-192]
    vaddps   ymm0, ymm3, [rdi-192]
    vmovaps  [r13-192], ymm0

    vmovaps  ymm4, [rsi-160]
    vaddps   ymm0, ymm4, [rdi-160]
    vmovaps  [r13-160], ymm0

    vmovaps  ymm5, [rsi-128]
    vaddps   ymm0, ymm5, [rdi-128]
    vmovaps  [r13-128], ymm0

    vmovaps  ymm6, [rsi-96]
    vaddps   ymm0, ymm6, [rdi-96]
    vmovaps  [r13-96], ymm0

    vmovaps  ymm7, [rsi-64]
    vaddps   ymm0, ymm7, [rdi-64]
    vmovaps  [r13-64], ymm0

    vmovaps  ymm1, [rsi-32]
    vaddps   ymm0, ymm1, [rdi-32]
    vmovaps  [r13-32], ymm0

    dec rbp
    jg .loop

    dec rax
    jg .outer

    xor edi,edi
    mov eax,231
    syscall