L1内存带宽:使用相差4096 + 64字节的地址,效率下降50%

Z b*_*son 10 c memory x86 caching avx

我想用英特尔处理器实现以下操作的最大带宽.

for(int i=0; i<n; i++) z[i] = x[i] + y[i]; //n=2048
Run Code Online (Sandbox Code Playgroud)

其中x,y和z是浮点数组.我在Haswell,Ivy Bridge和Westmere系统上这样做.

我最初分配了这样的内存

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;
Run Code Online (Sandbox Code Playgroud)

当我这样做时,我获得了每个系统预期的峰值带宽的大约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
Run Code Online (Sandbox Code Playgroud)

这意味着例如Haswell II仅观察48个字节/时钟周期(可能是一个时钟周期内的两次读取,另一次写入下一个时钟周期).

我打印出来的地址差b-ac-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;
Run Code Online (Sandbox Code Playgroud)

这几乎使我的峰值带宽增加了一倍,因此我现在可以获得约90%的峰值带宽.然而,当我尝试k=1它时,它回落到50%.我已经试过的其他值k,发现例如k=2,k=33,k=65只得到了峰值的50%,但例如k=10,k=32,k=63给了全速.我不明白这一点.

在Agner Fog的micrarchitecture手册中,他说存在与存储器地址的错误依赖关系,具有相同的设置和偏移

不能同时从间隔4 KB的地址读取和写入.

但这正是我看到最大利益的地方!当k=0内存地址完全相差2*4096字节时.Agner还谈到了Cache bank冲突.但Haswell和Westmere并不认为存在这些银行冲突,所以不应该解释我所观察到的.这是怎么回事!?

据我所知,OoO执行决定了哪个地址可以读写,所以即使数组的内存地址恰好相差4096字节也不一定意味着处理器同时读取&x[0]和写入&z[0]但是为什么会被一个单独关闭缓存行导致它窒息?

编辑:根据Evgeny Kluev的回答,我现在相信这就是Agner Fog所说的"虚假商店转发摊位".在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
Run Code Online (Sandbox Code Playgroud)

编辑:以下是每个系统的效率表k=0k=1.

               k=0      k=1        
Westmere:      99%      66%
Ivy Bridge:    98%      44%
Haswell:       90%      49%
Run Code Online (Sandbox Code Playgroud)

我想我可以解释这些数字,如果我假设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%
Run Code Online (Sandbox Code Playgroud)

这个理论非常有效.常春藤桥比我预期的要低一些,但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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Evg*_*uev 9

我想之间的差距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
Run Code Online (Sandbox Code Playgroud)

由于Haswell被认为没有银行冲突,唯一剩下的解释是内存地址之间的错误依赖(你已经在Agner Fog的微架构手册中找到了解释这个问题的适当位置).银行冲突和虚假共享之间的区别在于,银行冲突阻止在同一时钟周期内两次访问同一银行,而虚假共享阻止在您写入相同的偏移量之后读取4K内存中的某些偏移量(并且不仅仅是在相同的时钟周期内,也可以在写入后的几个时钟周期内).

由于您的代码(for k=0)从同一偏移量执行两次读取之后写入任何偏移量并且在很长时间内不会从中读取,因此这种情况应该被视为"最佳",因此我放置k=0在表的末尾.因为k=1你总是从最近被覆盖的偏移读取,这意味着错误共享,从而降低性能.k写入和读取之间的时间间隔越大,CPU内核就有更多机会将写入的数据传递到所有内存层次结构(这意味着读取和写入的两个地址转换,更新缓存数据和标记以及从缓存中获取数据,核心之间的数据同步,以及可能还有更多东西).k=12或者24个时钟(在我的CPU上)足以让每个写入的数据准备好进行后续读取操作,因此从这个值开始,性能将恢复正常.看起来与AMD的20多个时钟没有太大区别(正如@Mysticial所说).

  • 2)k = 33是坏的,因为k = 32,你有2个2048字节的间隙,或者`a`和`c`之间的4Kb; 并且4Kb与无间隙相同,因此k = 32与k = 0相同,k = 33与k = 1相同... 3)k = 12表示24个32字节块,每个需要1个时钟要处理,所以24个时钟. (2认同)

Had*_*ais 5

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)));
}
Run Code Online (Sandbox Code Playgroud)

对于k奇数正整数(例如1)的情况,此拆分消除了大多数4K别名。Haswell上实现的L1带宽提高了约50%。仍有改进的空间,例如,展开循环并找出不对装载和存储使用索引寻址模式的方法。

但是,此拆分无法消除的偶数值的4K混叠k。因此,对于的偶数值,需要使用其他拆分k。但是,当k为0时,无需拆分循环即可获得最佳性能。在这种情况下,性能同时在端口1、2、3、4和7上受后端限制。

同时执行加载和存储的某些情况下,可能会有几个周期的损失,但是在这种特定情况下,由于几乎没有这样的冲突(例如,并发加载的地址),这种损失基本上不存在并且商店之间的距离足够远)。另外,总的工作集大小适合L1,因此在第一次执行循环后就没有L1-L2流量了。

该答案的其余部分包括对该摘要的详细说明。


首先,观察三个数组的总大小为24KB。另外,由于您在执行主循环之前要初始化阵列,因此主循环中的大多数访问都将进入L1D,L1D的大小为32KB,在现代Intel处理器上为8路关联。因此,我们不必担心丢失或硬件预取。在这种情况下LD_BLOCKS_PARTIAL.ADDRESS_ALIAS,最重要的性能事件是,当涉及较新负载的部分地址比较导致与较早商店的匹配并且满足所有商店转发条件,但目标位置实际上不同时,发生此事件。英特尔将这种情况称为4K别名或错误的存储转发。可观察到的4K混叠性能损失取决于周围的代码。

通过测量cyclesLD_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节说:

当代码存储到一个存储位置时,就会出现4 KB的内存别名,此后不久,代码就会从另一个存储位置加载,它们之间的偏移为4 KB。例如,加载到线性地址0x400020之后是存储到线性地址0x401020。

加载和存储的地址位5-11具有相同的值,并且访问的字节偏移应部分或完全重叠。

也就是说,有两个必要条件,以便以后使用较早的存储来加载别名:

  • 两个线性地址的位5-11必须相等。
  • 访问的位置必须重叠(以便可以转发一些数据)。

在支持AVX-512的处理器上,在我看来,单个加载uop最多可以加载64个字节。所以我认为第一个条件的范围应该是6-11,而不是5-11。

下面的清单显示了基于AVX的(32字节)内存访问序列及其地址的最低有效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
.
.
.
Run Code Online (Sandbox Code Playgroud)

注意,当k = 0时,似乎没有负载满足4K混叠的两个条件。另一方面,当k = 1时,所有负载似乎都满足条件。但是,对于所有迭代和所有值手动进行此操作都很繁琐k。因此,我编写了一个程序,该程序基本上会生成内存访问的地址,并针对不同的值计算遭受4K别名的负载总数k。我面临的一个问题是,对于任何给定的负载,我们都不知道仍在存储缓冲区中的存储数量(尚未提交)。因此,我设计了模拟器,以便它可以针对的不同值使用不同的商店吞吐量k,这似乎更好地反映了真实处理器上实际发生的情况。该代码可以在这里找到。

下图显示了与LD_BLOCKS_PARTIAL.ADDRESS_ALIAS在Haswell上使用测量的数量相比,模拟器产生的4K混叠情况的数量。我已经针对每个值调整了模拟器中使用的商店吞吐量,k以使两条曲线尽可能相似。第二个图显示了模拟器中使用的并在Haswell上测量的反向存储吞吐量(总周期除以存储总数)。请注意,k = 0时的存储吞吐量无关紧要,因为无论如何都没有4K别名。由于每个存储有两个负载,因此反向负载吞吐量是反向存储吞吐量的一半。

在此处输入图片说明

在此处输入图片说明

显然,每个存储区保留在存储区缓冲区中的时间在Haswell和模拟器上是不同的,因此我需要使用不同的吞吐量来使两条曲线相似。该模拟器可用于显示商店吞吐量如何影响4K别名的数量。如果商店吞吐量非常接近1c / store,则4K混叠情况的数量会少得多。4K别名条件不会导致流水线刷新,但可能会导致RS重播uop。在这种情况下,我没有观察到任何重放。

我想我可以解释这些数字,如果我假设对于k = 1而言,写入和读取不能在同一时钟周期内发生。

当同时执行加载和存储时,实际上会有几个周期的损失,但是只有当加载和存储的地址在Haswell上的64字节(但不相等)内或在Ivy Bridge上的32字节以内时,它们才会发生和桑迪桥。在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 
Run Code Online (Sandbox Code Playgroud)