gpt4 book ai didi

c - 错误的 gcc 生成的程序集顺序,导致性能下降

转载 作者:太空狗 更新时间:2023-10-29 17:00:19 24 4
gpt4 key购买 nike

我有以下代码,它将数据从内存复制到 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>

注意最后 vmovdqa 的重新排序和 vmovntdq指示。与 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 以生成不太理想的代码而闻名,但我发现找到“鼓励”它生成更好的代码(当然,仅适用于 HitTest /瓶颈代码)的方法很有趣,而无需过多地进行微观管理。在这种特殊情况下,我查看了用于此类情况的三个“工具”:

  • 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

    完全没有任何优化,代码完全恶心,充满了不必要的 Action ,因此需要进行一些优化。 (以上使用 -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就是为了对比,本质上是做了一个 jmp , cmp , 和 jae在每次迭代中,这可能会产生非常糟糕的结果。

    注意:如果你对现实世界的代码做了类似的事情,请添加注释(特别是对于 __asm__ __volatile__ ("");),并记得定期检查所有可用的编译器,以确保代码不会被任何编译器编译得太糟糕。

    看着 Peter Cordes' excellent answer ,我决定进一步迭代这个函数,只是为了好玩。

    正如罗斯里奇在评论中提到的,当使用 _mm256_load_si256() 时指针未解除引用(在重新转换为对齐的 __m256i * 作为函数的参数之前),因此 volatile使用时无济于事 _mm256_load_si256() .在另一条评论中,Seb 提出了一种解决方法: _mm256_load_si256((__m256i []){ *(volatile __m256i *)(src) }) ,它为函数提供指向 src 的指针通过可变指针访问元素并将其转换为数组。对于简单的对齐加载,我更喜欢直接 volatile 指针;它符合我在代码中的意图。 (我确实瞄准了 KISS,虽然我经常只击中它的愚蠢部分。)

    在 x86-64 上,内循环的开始对齐为 16 字节,因此函数“头”部分中的操作次数并不重要。尽管如此,避免多余的二进制 AND(屏蔽要以字节为单位复制的数量的五个最低有效位)通常肯定是有用的。

    GCC 为此提供了两个选项。一个是 __builtin_assume_aligned() 内置的,它允许程序员将各种对齐信息传递给编译器。另一个是 typedef'ing 具有额外属性的类型,这里是 __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) 内存访问以及缓存行为(以及缓存与周围环境的交互)代码)对于此类事情至关重要,因此我认为对其进行微基准测试是没有意义的。)

    关于c - 错误的 gcc 生成的程序集顺序,导致性能下降,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25778302/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com