簡體   English   中英

L1內存帶寬:使用相差4096 + 64字節的地址,效率下降50%

[英]L1 memory bandwidth: 50% drop in efficiency using addresses which differ by 4096+64 bytes

我想用英特爾處理器實現以下操作的最大帶寬。

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

其中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;

當我這樣做時,我獲得了每個系統預期的峰值帶寬的大約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 II僅觀察48個字節/時鍾周期(可能是一個時鍾周期內的兩次讀取,另一次寫入下一個時鍾周期)。

我打印出bacb的地址差異,每個都是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=2k=33k=65僅獲得峰值的50%但是例如k=10k=32k=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

編輯:這是k=0k=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 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);
    }
}

我認為ab之間的差距並不重要。 bc之間只留下一個間隙后,我在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

由於Haswell被認為沒有銀行沖突,唯一剩下的解釋是內存地址之間的錯誤依賴(你已經在Agner Fog的微架構手冊中找到了解釋這個問題的適當位置)。 銀行沖突和虛假共享之間的區別在於,銀行沖突阻止在同一時鍾周期內訪問同一銀行兩次,而虛假共享阻止在您寫入相同的偏移量之后讀取4K內存中的某些偏移量(並且不僅僅是在相同的時鍾周期內,也可以在寫入后的幾個時鍾周期內)。

由於您的代碼(對於k=0從相同偏移量執行兩次讀取之后寫入任何偏移量並且在很長時間內不會從中讀取,因此這種情況應該被視為“最佳”,因此我將k=0在表的末尾。 對於k=1您始終從最近被覆蓋的偏移讀取,這意味着錯誤共享,從而降低性能。 寫入和讀取之間的k時間增加,CPU內核有更多機會將寫入的數據傳遞到所有內存層次結構(這意味着兩個地址轉換用於讀取和寫入,更新緩存數據和標記以及從緩存中獲取數據,核心之間的數據同步,可能還有更多的東西)。 k=12或24個時鍾(在我的CPU上)足以讓每個寫入的數據准備好進行后續讀取操作,因此從這個值開始,性能將恢復正常。 看起來與AMD的20多個時鍾沒有太大區別(正如@Mysticial所說)。

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)));
}

k是奇數正整數(例如1)時,這種分割消除了大多數4K混疊。 Haswell實現的L1帶寬提高了約50%。 例如,通過展開循環並找出不使用索引尋址模式進行加載和存儲的方法,仍有改進的余地。

但是,對於k偶數值,這種分割不會消除4K混疊。 因此,需要對k偶數值使用不同的分割。 然而,當k為0時,可以在不分割環的情況下實現最佳性能。 在這種情況下,性能同時在端口1,2,3,4和7上進行后端綁定。

在某些情況下,在同時執行加載和存儲時可能會有幾個周期的懲罰,但在這種特殊情況下,這種懲罰基本上不存在,因為基本上沒有這樣的沖突(即並發加載的地址)和商店相距甚遠)。 此外,總工作集大小適合L1,因此在第一次執行循環之后沒有L1-L2流量。

本答復的其余部分包括對本摘要的詳細解釋。


首先,觀察三個陣列的總大小為24KB。 此外,由於您在執行主循環之前初始化數組,因此主循環中的大多數訪問將進入L1D,其大小為32KB,在現代Intel處理器上為8路關聯。 因此,我們不必擔心未命中或硬件預取。 在這種情況下,最重要的性能事件是LD_BLOCKS_PARTIAL.ADDRESS_ALIAS ,當涉及較晚加載的部分地址比較導致與早期存儲匹配並且滿足所有商店轉發條件時發生,但目標位置實際上不同。 英特爾將此情況稱為4K別名或虛假存儲轉發。 4K混疊的可觀察性能損失取決於周圍的代碼。

通過測量cycles LD_BLOCKS_PARTIAL.ADDRESS_ALIASMEM_UOPS_RETIRED.ALL_LOADS ,我們可以看到,對於k所有值,其中實現的帶寬遠小於峰值帶寬, LD_BLOCKS_PARTIAL.ADDRESS_ALIASMEM_UOPS_RETIRED.ALL_LOADS幾乎相等。 此外,對於實現的帶寬接近峰值帶寬的k所有值,與MEM_UOPS_RETIRED.ALL_LOADS相比, LD_BLOCKS_PARTIAL.ADDRESS_ALIAS非常小。 這證實了由於大多數負載遭受4K混疊而發生帶寬降級。

英特爾優化手冊第12.8節說明如下:

當代碼存儲到一個內存位置時,會發生4 KB內存別名,之后不久,它會從不同的內存位置加載,它們之間的偏移量為4 KB。 例如,線性地址0x400020的加載跟隨存儲到線性地址0x401020。

加載和存儲對於其地址的位5-11具有相同的值,並且所訪問的字節偏移應該具有部分或完全重疊。

也就是說,稍后加載與早期商店的別名有兩個必要條件:

  • 兩個線性地址的第5-11位必須相等。
  • 訪問的位置必須重疊(以便可以轉發一些數據)。

在支持AVX-512的處理器上,在我看來,單個加載uop最多可以加載64個字節。 所以我認為第一個條件的范圍應該是6-11而不是5-11。

下面的清單顯示了基於AVX(32字節)的存儲器訪問序列以及它們的兩個不同k值的地址的最低有效12位。

======
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所有值,手動執行此操作非常繁瑣。 所以我編寫了一個程序,它基本上生成了內存訪問的地址,並計算了不同k值的4K混疊的負載總數。 我遇到的一個問題是,對於任何給定的負載,我們不知道仍在存儲緩沖區中的存儲數量(尚未提交)。 因此,我設計了模擬器,以便它可以針對不同的k值使用不同的存儲吞吐量,這似乎更好地反映了真實處理器上實際發生的情況。 代碼可以在這里找到。

下圖顯示了使用Haswell上的LD_BLOCKS_PARTIAL.ADDRESS_ALIAS ,模擬器生成的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 

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM