簡體   English   中英

錯誤的gcc生成的裝配順序,導致性能損失

[英]Wrong gcc generated assembly ordering, results in performance hit

我有以下代碼,它將數據從內存復制到DMA緩沖區:

for (; likely(l > 0); l-=128)
{
    __m256i m0 = _mm256_load_si256( (__m256i*) (src) );
    __m256i m1 = _mm256_load_si256( (__m256i*) (src+32) );
    __m256i m2 = _mm256_load_si256( (__m256i*) (src+64) );
    __m256i m3 = _mm256_load_si256( (__m256i*) (src+96) );

    _mm256_stream_si256( (__m256i *) (dst), m0 );
    _mm256_stream_si256( (__m256i *) (dst+32), m1 );
    _mm256_stream_si256( (__m256i *) (dst+64), m2 );
    _mm256_stream_si256( (__m256i *) (dst+96), m3 );

    src += 128;
    dst += 128;
}

這就是gcc程序集輸出的樣子:

405280:       c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
405285:       c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
40528a:       c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
40528f:       c5 fd 6f 18             vmovdqa (%rax),%ymm3
405293:       48 83 e8 80             sub    $0xffffffffffffff80,%rax
405297:       c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
40529c:       c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
4052a1:       c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
4052a6:       c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
4052aa:       48 83 ea 80             sub    $0xffffffffffffff80,%rdx
4052ae:       48 39 c8                cmp    %rcx,%rax
4052b1:       75 cd                   jne    405280 <sender_body+0x6e0>

請注意最后一個vmovdqavmovntdq指令的重新排序。 使用gcc生成的代碼,我能夠在我的應用程序中達到每秒約10 227 571個數據包的吞吐量。

接下來,我在hexeditor中手動重新排序指令。 這意味着現在循環看起來如下:

405280:       c5 fd 6f 18             vmovdqa (%rax),%ymm3
405284:       c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
405289:       c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
40528e:       c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
405293:       48 83 e8 80             sub    $0xffffffffffffff80,%rax
405297:       c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
40529b:       c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
4052a0:       c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
4052a5:       c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
4052aa:       48 83 ea 80             sub    $0xffffffffffffff80,%rdx
4052ae:       48 39 c8                cmp    %rcx,%rax
4052b1:       75 cd                   jne    405280 <sender_body+0x6e0>

通過正確排序的指令,我得到每秒~13 668 313個數據包。 因此很明顯, gcc引入的重新排序會降低性能。

你有遇到過嗎? 這是一個已知的錯誤還是應該填寫錯誤報告?

編譯標志:

-O3 -pipe -g -msse4.1 -mavx

我的gcc版本:

gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)

我覺得這個問題很有意思。 GCC以生成不是最優的代碼而聞名,但我發現找到“鼓勵”它來生成更好的代碼(當然只針對最熱門/瓶頸代碼)的方法很有吸引力,而不需要過多地進行微觀管理。 在這個特殊情況下,我查看了三種用於此類情況的“工具”:

  • volatile :如果重要的是內存訪問按特定順序發生,那么volatile是一個合適的工具。 請注意,它可能過度,並且每次解除引用volatile指針時都會導致單獨的加載。

    SSE / AVX加載/存儲內在函數不能與volatile指針一起使用,因為它們是函數。 使用像_mm256_load_si256((volatile __m256i *)src); 隱式地將它強制轉換為const __m256i* ,失去volatile限定符。

    但是,我們可以直接取消引用volatile指針。 (只有當我們需要告訴編譯器數據可能是未對齊的,或者我們想要一個流存儲時,才需要加載/存儲內在函數。)

     m0 = ((volatile __m256i *)src)[0]; m1 = ((volatile __m256i *)src)[1]; m2 = ((volatile __m256i *)src)[2]; m3 = ((volatile __m256i *)src)[3]; 

    不幸的是,這對商店沒有幫助,因為我們想要發布流媒體商店。 A *(volatile...)dst = tmp; 不會給我們想要的東西。

  • __asm__ __volatile__ (""); 作為編譯器重新排序的障礙。

    這是GNU C編寫的一個編譯器內存屏障。 (停止編譯時重新排序而不發出像mfence這樣的實際屏障指令)。 它阻止編譯器在此語句中重新排序內存訪問。

  • 使用循環結構的索引限制。

    GCC以非常差的寄存器使用而聞名。 早期版本在寄存器之間進行了許多不必要的移動,盡管現在這種移動很少。 但是,在許多版本的GCC上對x86-64進行測試表明,在循環中,最好使用索引限制而不是獨立的循環變量來獲得最佳結果。

結合以上所有內容,我構造了以下函數(經過幾次迭代):

#include <stdlib.h>
#include <immintrin.h>

#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)

void copy(void *const destination, const void *const source, const size_t bytes)
{
    __m256i       *dst = (__m256i *)destination;
    const __m256i *src = (const __m256i *)source;
    const __m256i *end = (const __m256i *)source + bytes / sizeof (__m256i);

    while (likely(src < end)) {
        const __m256i m0 = ((volatile const __m256i *)src)[0];
        const __m256i m1 = ((volatile const __m256i *)src)[1];
        const __m256i m2 = ((volatile const __m256i *)src)[2];
        const __m256i m3 = ((volatile const __m256i *)src)[3];

        _mm256_stream_si256( dst,     m0 );
        _mm256_stream_si256( dst + 1, m1 );
        _mm256_stream_si256( dst + 2, m2 );
        _mm256_stream_si256( dst + 3, m3 );

        __asm__ __volatile__ ("");

        src += 4;
        dst += 4;
    }
}

使用GCC-4.8.4編譯它( example.c

gcc -std=c99 -mavx2 -march=x86-64 -mtune=generic -O2 -S example.c

收益率( example.s ):

        .file   "example.c"
        .text
        .p2align 4,,15
        .globl  copy
        .type   copy, @function
copy:
.LFB993:
        .cfi_startproc
        andq    $-32, %rdx
        leaq    (%rsi,%rdx), %rcx
        cmpq    %rcx, %rsi
        jnb     .L5
        movq    %rsi, %rax
        movq    %rdi, %rdx
        .p2align 4,,10
        .p2align 3
.L4:
        vmovdqa (%rax), %ymm3
        vmovdqa 32(%rax), %ymm2
        vmovdqa 64(%rax), %ymm1
        vmovdqa 96(%rax), %ymm0
        vmovntdq        %ymm3, (%rdx)
        vmovntdq        %ymm2, 32(%rdx)
        vmovntdq        %ymm1, 64(%rdx)
        vmovntdq        %ymm0, 96(%rdx)
        subq    $-128, %rax
        subq    $-128, %rdx
        cmpq    %rax, %rcx
        ja      .L4
        vzeroupper
.L5:
        ret
        .cfi_endproc
.LFE993:
        .size   copy, .-copy
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

實際編譯( -c而不是-S )代碼的反匯編是

0000000000000000 <copy>:
   0:   48 83 e2 e0             and    $0xffffffffffffffe0,%rdx
   4:   48 8d 0c 16             lea    (%rsi,%rdx,1),%rcx
   8:   48 39 ce                cmp    %rcx,%rsi
   b:   73 41                   jae    4e <copy+0x4e>
   d:   48 89 f0                mov    %rsi,%rax
  10:   48 89 fa                mov    %rdi,%rdx
  13:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  18:   c5 fd 6f 18             vmovdqa (%rax),%ymm3
  1c:   c5 fd 6f 50 20          vmovdqa 0x20(%rax),%ymm2
  21:   c5 fd 6f 48 40          vmovdqa 0x40(%rax),%ymm1
  26:   c5 fd 6f 40 60          vmovdqa 0x60(%rax),%ymm0
  2b:   c5 fd e7 1a             vmovntdq %ymm3,(%rdx)
  2f:   c5 fd e7 52 20          vmovntdq %ymm2,0x20(%rdx)
  34:   c5 fd e7 4a 40          vmovntdq %ymm1,0x40(%rdx)
  39:   c5 fd e7 42 60          vmovntdq %ymm0,0x60(%rdx)
  3e:   48 83 e8 80             sub    $0xffffffffffffff80,%rax
  42:   48 83 ea 80             sub    $0xffffffffffffff80,%rdx
  46:   48 39 c1                cmp    %rax,%rcx
  49:   77 cd                   ja     18 <copy+0x18>
  4b:   c5 f8 77                vzeroupper 
  4e:   c3                      retq

在沒有任何優化的情況下,代碼完全令人作嘔,充滿了不必要的動作,因此需要進行一些優化。 (以上使用-O2 ,這通常是我使用的優化級別。)

如果優化大小( -Os ),乍一看代碼看起來很棒,

0000000000000000 <copy>:
   0:   48 83 e2 e0             and    $0xffffffffffffffe0,%rdx
   4:   48 01 f2                add    %rsi,%rdx
   7:   48 39 d6                cmp    %rdx,%rsi
   a:   73 30                   jae    3c <copy+0x3c>
   c:   c5 fd 6f 1e             vmovdqa (%rsi),%ymm3
  10:   c5 fd 6f 56 20          vmovdqa 0x20(%rsi),%ymm2
  15:   c5 fd 6f 4e 40          vmovdqa 0x40(%rsi),%ymm1
  1a:   c5 fd 6f 46 60          vmovdqa 0x60(%rsi),%ymm0
  1f:   c5 fd e7 1f             vmovntdq %ymm3,(%rdi)
  23:   c5 fd e7 57 20          vmovntdq %ymm2,0x20(%rdi)
  28:   c5 fd e7 4f 40          vmovntdq %ymm1,0x40(%rdi)
  2d:   c5 fd e7 47 60          vmovntdq %ymm0,0x60(%rdi)
  32:   48 83 ee 80             sub    $0xffffffffffffff80,%rsi
  36:   48 83 ef 80             sub    $0xffffffffffffff80,%rdi
  3a:   eb cb                   jmp    7 <copy+0x7>
  3c:   c3                      retq

直到你注意到最后一個jmp是比較,基本上在每次迭代時都會執行jmpcmpjae ,這可能會產生非常差的結果。

注意:如果你為實際代碼做類似的事情,請添加注釋(特別是對於__asm__ __volatile__ (""); ),並記得定期檢查所有可用的編譯器,以確保代碼編譯得不是太糟糕任何。


看看Peter Cordes的優秀答案 ,我決定進一步迭代這個功能,只是為了好玩。

正如Ross Ridge在評論中提到的那樣,當使用_mm256_load_si256() ,指針不會被解除引用(在重新轉換為對齊__m256i *作為函數的參數之前),因此當使用_mm256_load_si256()時, volatile_mm256_load_si256() 在另一個評論中,Seb提出了一個解決方法: _mm256_load_si256((__m256i []){ *(volatile __m256i *)(src) }) ,它通過一個易失性指針訪問該元素並將其轉換為該函數,為該函數提供指向src的指針數組。 對於簡單的對齊加載,我更喜歡直接易失性指針; 它符合我在代碼中的意圖。 (我確實以KISS為目標,雖然我經常只打它的愚蠢部分。)

在x86-64上,內部循環的開始對齊為16個字節,因此函數“header”部分中的操作數量並不重要。 盡管如此,一般來說,避免多余的二進制AND(屏蔽要以字節為單位的數量的五個最低有效位)肯定是有用的。

GCC為此提供了兩種選擇。 一個是內置的__builtin_assume_aligned() ,它允許程序員將各種對齊信息傳遞給編譯器。 另一種是typedef一個具有額外屬性的類型,這里是__attribute__((aligned (32))) ,例如,它可以用來傳達函數參數的__attribute__((aligned (32))) 這兩個都應該在clang中可用(雖然支持是最近的,而不是3.5),並且可能在其他如icc中可用(盡管ICC,AFAIK,使用__assume_aligned() )。

減輕GCC注冊混亂的一種方法是使用輔助函數。 一些進一步的迭代之后,我來到這, another.c

#include <stdlib.h>
#include <immintrin.h>

#define likely(x)   __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)

#if (__clang_major__+0 >= 3)
#define IS_ALIGNED(x, n) ((void *)(x))
#elif (__GNUC__+0 >= 4)
#define IS_ALIGNED(x, n) __builtin_assume_aligned((x), (n))
#else
#define IS_ALIGNED(x, n) ((void *)(x))
#endif

typedef __m256i __m256i_aligned __attribute__((aligned (32)));


void do_copy(register          __m256i_aligned *dst,
             register volatile __m256i_aligned *src,
             register          __m256i_aligned *end)
{
    do {
        register const __m256i m0 = src[0];
        register const __m256i m1 = src[1];
        register const __m256i m2 = src[2];
        register const __m256i m3 = src[3];

        __asm__ __volatile__ ("");

        _mm256_stream_si256( dst,     m0 );
        _mm256_stream_si256( dst + 1, m1 );
        _mm256_stream_si256( dst + 2, m2 );
        _mm256_stream_si256( dst + 3, m3 );

        __asm__ __volatile__ ("");

        src += 4;
        dst += 4;

    } while (likely(src < end));
}

void copy(void *dst, const void *src, const size_t bytes)
{
    if (bytes < 128)
        return;

    do_copy(IS_ALIGNED(dst, 32),
            IS_ALIGNED(src, 32),
            IS_ALIGNED((void *)((char *)src + bytes), 32));
}

其中匯編了gcc -march=x86-64 -mtune=generic -mavx2 -O2 -S another.c到本質上(為簡潔省略了注釋和指令):

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        vzeroupper
        ret

copy:
        cmpq     $127, %rdx
        ja       .L8
        rep ret
.L8:
        addq     %rsi, %rdx
        jmp      do_copy

-O3進一步優化只是內聯輔助函數,

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        vzeroupper
        ret

copy:
        cmpq     $127, %rdx
        ja       .L10
        rep ret
.L10:
        leaq     (%rsi,%rdx), %rax
.L8:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rsi, %rax
        ja       .L8
        vzeroupper
        ret

甚至使用-Os生成的代碼非常好,

do_copy:
.L3:
        vmovdqa  (%rsi), %ymm3
        vmovdqa  32(%rsi), %ymm2
        vmovdqa  64(%rsi), %ymm1
        vmovdqa  96(%rsi), %ymm0
        vmovntdq %ymm3, (%rdi)
        vmovntdq %ymm2, 32(%rdi)
        vmovntdq %ymm1, 64(%rdi)
        vmovntdq %ymm0, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .L3
        ret

copy:
        cmpq     $127, %rdx
        jbe      .L5
        addq     %rsi, %rdx
        jmp      do_copy
.L5:
        ret

當然,如果沒有優化,GCC-4.8.4仍會產生相當糟糕的代碼。 使用clang-3.5 -march=x86-64 -mtune=generic -mavx2 -O2-Os我們基本上得到了

do_copy:
.LBB0_1:
        vmovaps  (%rsi), %ymm0
        vmovaps  32(%rsi), %ymm1
        vmovaps  64(%rsi), %ymm2
        vmovaps  96(%rsi), %ymm3
        vmovntps %ymm0, (%rdi)
        vmovntps %ymm1, 32(%rdi)
        vmovntps %ymm2, 64(%rdi)
        vmovntps %ymm3, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .LBB0_1
        vzeroupper
        retq

copy:
        cmpq     $128, %rdx
        jb       .LBB1_3
        addq     %rsi, %rdx
.LBB1_2:
        vmovaps  (%rsi), %ymm0
        vmovaps  32(%rsi), %ymm1
        vmovaps  64(%rsi), %ymm2
        vmovaps  96(%rsi), %ymm3
        vmovntps %ymm0, (%rdi)
        vmovntps %ymm1, 32(%rdi)
        vmovntps %ymm2, 64(%rdi)
        vmovntps %ymm3, 96(%rdi)
        subq     $-128, %rsi
        subq     $-128, %rdi
        cmpq     %rdx, %rsi
        jb       .LBB1_2
.LBB1_3:
        vzeroupper
        retq

我喜歡another.c代碼(它適合我的編碼風格),我對GCC-4.8.4和clang-3.5在-O1-O2-O3-Os兩者上生成的代碼感到滿意,所以我認為這對我來說已經足夠了。 (但是,請注意,我實際上沒有對此進行基准測試,因為我沒有相關代碼。我們使用時間和非時間(nt)內存訪問,以及緩存行為(以及緩存與周圍環境的交互)代碼)對於像這樣的事情是至關重要的,所以我認為微觀標記是沒有意義的。)

首先,普通人使用gcc -O3 -march=native -S然后編輯.s來測試對編譯器輸出的小修改。 我希望你有趣的十六進制編輯改變。 :P你也可以使用Agner Fog優秀的objconv進行反匯編,可以通過選擇NASM,YASM,MASM或AT&T語法將其組合成二進制文件。


使用與Nominal Animal相同的一些想法,我制作了一個版本,編譯成同樣好的asm 我有信心為什么它會編譯成好的代碼,而且我猜測為什么這個順序非常重要:

CPU只有少量(~10?) 寫入組合填充緩沖區用於NT加載/存儲

請參閱此文章,了解如何使用流式加載視頻內存進行復制,以及使用流式存儲寫入主內存 實際上通過小緩沖區(遠小於L1)反彈數據實際上更快,以避免流加載和流存儲競爭填充緩沖區(尤其是無序執行)。 請注意,從正常內存使用“流”NT加載是沒有用的。 據我了解,流加載僅對I / O有用(包括視頻RAM,它映射到Uncacheable Software-Write-Combining(USWC)區域中的CPU地址空間)。 主存儲器RAM映射為WB(寫回),因此允許CPU以推測方式預取它並對其進行緩存,這與USWC不同。 無論如何,即使我正在鏈接一篇關於使用流媒體加載的文章,我也不建議使用流媒體加載 這只是為了說明填充緩沖區的爭用幾乎肯定是gcc奇怪的代碼導致一個大問題的原因,而不是正常的非NT存儲。

另請參閱John McAlpin在此主題末尾的評論,另一個來源確認WC一次存儲到多個緩存行可能會大幅放緩。

gcc的原始代碼輸出(對於某些我無法想象的腦死亡原因)存儲了第一個高速緩存行的第二半,然后是第二個高速緩存行的兩半,然后是第一個高速緩存行的第一半。 可能有時候第一個高速緩存行的寫入組合緩沖區在寫入兩半之前都會被刷新,從而導致外部總線的使用效率降低。

clang沒有對我們的3個版本(我的,OP和Nominal Animal的)中的任何一個進行任何奇怪的重新排序。


無論如何,使用編譯器阻止停止編譯器重新排序但不發出屏障指令是阻止它的一種方法。 在這種情況下,它是一種在頭部命中編譯器並說“愚蠢的編譯器,不要那樣做”的方法。 我不認為你通常需要在任何地方都這樣做,但顯然你不能相信gcc與寫合並存儲(訂購真的很重要)。 因此,在使用NT加載和/或存儲時,至少使用您正在開發的編譯器來查看asm可能是個好主意。 我已經為gcc報道了這個 Richard Biener指出-fno-schedule-insns2是一種解決方法。

Linux(內核)已經有一個barrier()宏,它充當編譯器內存屏障。 幾乎可以肯定它只是一個GNU asm volatile("") 在Linux之外,您可以繼續使用該GNU擴展,也可以使用C11 stdatomic.h工具。 它們與C ++ 11 std::atomic工具基本相同,AFAIK語義相同(謝天謝地)。

我在每家商店之間設置了一道屏障,因為無論如何都沒有有用的重新排序,它們是免費的。 事實證明,循環中只有一個屏障可以很好地保持一切順序,這就是Nominal Animal的答案所做的。 它實際上並不禁止編譯器重新排序沒有隔離它們的屏障的商店; 編譯器只是選擇不。 這就是我在每家商店之間徘徊的原因。


我只問編譯器寫入屏障,因為我希望只有NT存儲的順序才有意義,而不是負載。 即使是交替的加載和存儲指令也可能無關緊要,因為OOO執行無論如何都會管理所有內容。 (請注意,英特爾的copy-from-video-mem文章甚至使用了mfence來避免在進行流媒體存儲和流式傳輸加載之間重疊。)

atomic_signal_fence不會直接記錄所有不同的內存排序選項。 atomic_thread_fence的C ++頁面是cppreference上的一個位置,其中有一些示例和更多內容。

這就是我沒有使用Nominal Animal將src聲明為指向易失性的想法的原因。 gcc決定以與商店相同的順序保持負載。


鑒於此,僅展開2可能不會在微基准測試中產生任何吞吐量差異,並將在生產中節省uop緩存空間。 每次迭代仍然會執行完整的緩存行,這似乎很好。

SnB系列CPU不能微融合2-reg尋址模式 ,因此最小化循環開銷(獲取指向src和dst結尾的指針,然后將負指數向上計數為零)的明顯方法不起作用。 商店不會微熔。 盡管如此,你很快就會將填充緩沖區填充到額外的uops無關緊要的程度。 該循環可能在每個周期幾乎沒有接近4個uop。

仍然有一種方法可以減少循環開銷:使用我可怕的丑陋和不可讀的C語言來使編譯器只執行一個sub (和cmp/jcc )作為循環開銷,根本不進行展開會使4-uop循環,即使在SnB上也應該在每個時鍾的一次迭代中發出。 (注意, vmovntdq是AVX2,而vmovntps只是AVX1。在這段代碼中,Clang已經使用vmovaps / vmovntps作為si256內在函數!它們具有相同的對齊要求,並不關心它們存儲的是什么位。它不保存任何insn字節,只有兼容性。)


請參閱第一段,了解與此相關的一個Godbolt鏈接。

我猜你在Linux內核中這樣做了,所以我輸入了適當的#ifdef所以這應該是正確的內核代碼或者為用戶空間編譯時。

#include <stdint.h>
#include <immintrin.h>

#ifdef __KERNEL__  // linux has it's own macro
//#define compiler_writebarrier()   __asm__ __volatile__ ("")
#define compiler_writebarrier()   barrier()
#else
// Use C11 instead of a GNU extension, for portability to other compilers
#include <stdatomic.h>
// unlike a single store-release, a release barrier is a StoreStore barrier.
// It stops all earlier writes from being delayed past all following stores
// Note that this is still only a compiler barrier, so no SFENCE is emitted,
// even though we're using NT stores.  So from another core's perpsective, our
// stores can become globally out of order.
#define compiler_writebarrier()   atomic_signal_fence(memory_order_release)
// this purposely *doesn't* stop load reordering.  
// In this case gcc loads in the same order it stores, regardless.  load ordering prob. makes much less difference
#endif

void copy_pjc(void *const destination, const void *const source, const size_t bytes)
{
          __m256i *dst  = destination;
    const __m256i *src  = source;
    const __m256i *dst_endp = (destination + bytes); // clang 3.7 goes berserk with intro code with this end condition
        // but with gcc it saves an AND compared to Nominal's bytes/32:

    // const __m256i *dst_endp = dst + bytes/sizeof(*dst); // force the compiler to mask to a round number


    #ifdef __KERNEL__
    kernel_fpu_begin();  // or preferably higher in the call tree, so lots of calls are inside one pair
    #endif

    // bludgeon the compiler into generating loads with two-register addressing modes like [rdi+reg], and stores to [rdi]
    // saves one sub instruction in the loop.
    //#define ADDRESSING_MODE_HACK
    //intptr_t src_offset_from_dst = (src - dst);
    // generates clunky intro code because gcc can't assume void pointers differ by a multiple of 32

    while (dst < dst_endp)  { 
#ifdef ADDRESSING_MODE_HACK
      __m256i m0 = _mm256_load_si256( (dst + src_offset_from_dst) + 0 );
      __m256i m1 = _mm256_load_si256( (dst + src_offset_from_dst) + 1 );
      __m256i m2 = _mm256_load_si256( (dst + src_offset_from_dst) + 2 );
      __m256i m3 = _mm256_load_si256( (dst + src_offset_from_dst) + 3 );
#else
      __m256i m0 = _mm256_load_si256( src + 0 );
      __m256i m1 = _mm256_load_si256( src + 1 );
      __m256i m2 = _mm256_load_si256( src + 2 );
      __m256i m3 = _mm256_load_si256( src + 3 );
#endif

      _mm256_stream_si256( dst+0, m0 );
      compiler_writebarrier();   // even one barrier is enough to stop gcc 5.3 reordering anything
      _mm256_stream_si256( dst+1, m1 );
      compiler_writebarrier();   // but they're completely free because we are sure this store ordering is already optimal
      _mm256_stream_si256( dst+2, m2 );
      compiler_writebarrier();
      _mm256_stream_si256( dst+3, m3 );
      compiler_writebarrier();

      src += 4;
      dst += 4;
    }

  #ifdef __KERNEL__
  kernel_fpu_end();
  #endif

}

它編譯為(gcc 5.3.0 -O3 -march=haswell ):

copy_pjc:
        # one insn shorter than Nominal Animal's: doesn't mask the count to a multiple of 32.
        add     rdx, rdi  # dst_endp, destination
        cmp     rdi, rdx  # dst, dst_endp
        jnb     .L7       #,
.L5:
        vmovdqa ymm3, YMMWORD PTR [rsi]   # MEM[base: src_30, offset: 0B], MEM[base: src_30, offset: 0B]
        vmovdqa ymm2, YMMWORD PTR [rsi+32]        # D.26928, MEM[base: src_30, offset: 32B]
        vmovdqa ymm1, YMMWORD PTR [rsi+64]        # D.26928, MEM[base: src_30, offset: 64B]
        vmovdqa ymm0, YMMWORD PTR [rsi+96]        # D.26928, MEM[base: src_30, offset: 96B]
        vmovntdq        YMMWORD PTR [rdi], ymm3 #* dst, MEM[base: src_30, offset: 0B]
        vmovntdq        YMMWORD PTR [rdi+32], ymm2      #, D.26928
        vmovntdq        YMMWORD PTR [rdi+64], ymm1      #, D.26928
        vmovntdq        YMMWORD PTR [rdi+96], ymm0      #, D.26928
        sub     rdi, -128 # dst,
        sub     rsi, -128 # src,
        cmp     rdx, rdi  # dst_endp, dst
        ja      .L5 #,
        vzeroupper
.L7:

Clang做了一個非常相似的循環,但是介紹要長得多:clang並不認為srcdest實際上都是對齊的。 如果沒有32B對齊,它可能沒有利用負載和存儲將會出錯的知識? (它知道它可以使用...aps指令而不是...dqa ,所以它肯定會對gcc(它們更經常總是變成相關指令)的內在函數進行更多編譯器式優化...dqa可以轉一對例如,左/右向量從常量轉換為掩碼。)

暫無
暫無

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

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