为啥这个 AVX 代码比较慢?

Posted

技术标签:

【中文标题】为啥这个 AVX 代码比较慢?【英文标题】:Why is this AVX code slower?为什么这个 AVX 代码比较慢? 【发布时间】:2017-08-19 14:20:58 【问题描述】:

更新日期: 2017 年 8 月 19 日 16:49 UTC

我正在编写一个 AVX 代码,将一个具有 40 亿个分量的向量乘以一个常数,但是,我认为我的小型(我希望)优化的 AVX 代码和长标量编译器优化版本之间没有区别。

两个版本的运行时间都在 410 毫秒 - 400 毫秒之间。

有人能告诉我为什么会这样吗? 为什么编译器代码生成的大型程序集即使更大也需要几乎相同的时间?

这是一个重要的问题,因为如果像这种乘法这样的小型计算没有改进,那么在 Intel Core CPU 中使用手动代码是没有意义的。可能在 Intel Xeon(具有 16 个组件)中或更复杂的计算中。

我正在使用带有参数的 G++ 进行编译: g++ -O3 -mtune=native -march=native -mavx -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/Test AVX.d" -MT"src/Test\ AVX.d" -o "src/Test AVX.o" "../src/Test AVX.cpp"

我的 CPU 是 Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz。

有AVX代码:

/**
 * Run AVX Code
 */
void AVX() 

    // Loop control
    uint_fast32_t loop = 0;

    // The constant
    __m256 _const = _mm256_set1_ps(5.0f);

    // The register for multiplication
    __m256 _ymm0 = _mm256_setzero_ps();

    // A "buffer" between the vector and the YMM0 register
    float f_data[8];


    // The main loop
    for ( loop = 0  ; loop < SIZE ; loop = loop + 8 ) 

        // Load to buffer
        f_data[0] = vector[loop];
        f_data[1] = vector[loop+1];
        f_data[2] = vector[loop+2];
        f_data[3] = vector[loop+3];
        f_data[4] = vector[loop+4];
        f_data[5] = vector[loop+5];
        f_data[6] = vector[loop+6];
        f_data[7] = vector[loop+7];

        /*
         * I tried to use pointers insted to copy
         * the data, but the software crash
         *
         * float **f_data;
         * f_data = float*[8];
         *
         * f_data[0] = &vector[loop];
         * ...
         *
         */


        // Load to XMM and YMM Registers
        _ymm0 = _mm256_load_ps(f_data);

        // Do the multiplication
        _ymm0 =  _mm256_mul_ps(_ymm0,_const);

        // Copy the results from the register to the "buffer"
        _mm256_store_ps(f_data,_ymm0);

        // Copy from the "buffer" to the vector
        vector[loop] = f_data[0];
        vector[loop+1] = f_data[1];
        vector[loop+2] = f_data[2];
        vector[loop+3] = f_data[3];
        vector[loop+4] = f_data[4];
        vector[loop+5] = f_data[5];
        vector[loop+6] = f_data[6];
        vector[loop+7] = f_data[7];


    


组装好的 AVX:

0000000000400de0 <_Z3AVXv>:
  400de0:   48 8b 05 b1 13 20 00    mov    rax,QWORD PTR [rip+0x2013b1]        # 602198 <vector>
  400de7:   c5 fc 28 0d 71 06 00    vmovaps ymm1,YMMWORD PTR [rip+0x671]        # 401460 <_IO_stdin_used+0x40>
  400dee:   00 
  400def:   48 8d 90 00 00 00 40    lea    rdx,[rax+0x40000000]
  400df6:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  400dfd:   00 00 00 
  400e00:   c5 f4 59 00             vmulps ymm0,ymm1,YMMWORD PTR [rax]
  400e04:   48 83 c0 20             add    rax,0x20
  400e08:   c5 fc 11 40 e0          vmovups YMMWORD PTR [rax-0x20],ymm0
  400e0d:   48 39 c2                cmp    rdx,rax
  400e10:   75 ee                   jne    400e00 <_Z3AVXv+0x20>
  400e12:   c5 f8 77                vzeroupper 
  400e15:   c3                      ret    
  400e16:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  400e1d:   00 00 00 

连续版本:

/**
 * Run Compiler optimized version
 */
void Serial() 

    uint_fast32_t loop;

    // Do the multiplication
    for ( loop = 0 ; loop < SIZE ; loop ++)
        vector[loop] *= 5;


连载组装:

它更大,移动数据更多次,花费几乎相同的时间。怎么可能?

0000000000400e80 <_Z6Serialv>:
  400e80:   48 8b 35 11 13 20 00    mov    rsi,QWORD PTR [rip+0x201311]        # 602198 <vector>
  400e87:   48 89 f0                mov    rax,rsi
  400e8a:   48 c1 e8 02             shr    rax,0x2
  400e8e:   48 f7 d8                neg    rax
  400e91:   83 e0 07                and    eax,0x7
  400e94:   0f 84 96 01 00 00       je     401030 <_Z6Serialv+0x1b0>
  400e9a:   c5 fa 10 05 7a 04 00    vmovss xmm0,DWORD PTR [rip+0x47a]        # 40131c <_IO_stdin_used+0x1c>
  400ea1:   00 
  400ea2:   c5 fa 59 0e             vmulss xmm1,xmm0,DWORD PTR [rsi]
  400ea6:   c5 fa 11 0e             vmovss DWORD PTR [rsi],xmm1
  400eaa:   48 83 f8 01             cmp    rax,0x1
  400eae:   0f 84 8c 01 00 00       je     401040 <_Z6Serialv+0x1c0>
  400eb4:   c5 fa 59 4e 04          vmulss xmm1,xmm0,DWORD PTR [rsi+0x4]
  400eb9:   c5 fa 11 4e 04          vmovss DWORD PTR [rsi+0x4],xmm1
  400ebe:   48 83 f8 02             cmp    rax,0x2
  400ec2:   0f 84 89 01 00 00       je     401051 <_Z6Serialv+0x1d1>
  400ec8:   c5 fa 59 4e 08          vmulss xmm1,xmm0,DWORD PTR [rsi+0x8]
  400ecd:   c5 fa 11 4e 08          vmovss DWORD PTR [rsi+0x8],xmm1
  400ed2:   48 83 f8 03             cmp    rax,0x3
  400ed6:   0f 84 86 01 00 00       je     401062 <_Z6Serialv+0x1e2>
  400edc:   c5 fa 59 4e 0c          vmulss xmm1,xmm0,DWORD PTR [rsi+0xc]
  400ee1:   c5 fa 11 4e 0c          vmovss DWORD PTR [rsi+0xc],xmm1
  400ee6:   48 83 f8 04             cmp    rax,0x4
  400eea:   0f 84 2d 01 00 00       je     40101d <_Z6Serialv+0x19d>
  400ef0:   c5 fa 59 4e 10          vmulss xmm1,xmm0,DWORD PTR [rsi+0x10]
  400ef5:   c5 fa 11 4e 10          vmovss DWORD PTR [rsi+0x10],xmm1
  400efa:   48 83 f8 05             cmp    rax,0x5
  400efe:   0f 84 6f 01 00 00       je     401073 <_Z6Serialv+0x1f3>
  400f04:   c5 fa 59 4e 14          vmulss xmm1,xmm0,DWORD PTR [rsi+0x14]
  400f09:   c5 fa 11 4e 14          vmovss DWORD PTR [rsi+0x14],xmm1
  400f0e:   48 83 f8 06             cmp    rax,0x6
  400f12:   0f 84 6c 01 00 00       je     401084 <_Z6Serialv+0x204>
  400f18:   c5 fa 59 46 18          vmulss xmm0,xmm0,DWORD PTR [rsi+0x18]
  400f1d:   41 b9 f9 ff ff 0f       mov    r9d,0xffffff9
  400f23:   41 ba 07 00 00 00       mov    r10d,0x7
  400f29:   c5 fa 11 46 18          vmovss DWORD PTR [rsi+0x18],xmm0
  400f2e:   41 b8 00 00 00 10       mov    r8d,0x10000000
  400f34:   c5 fc 28 0d 04 04 00    vmovaps ymm1,YMMWORD PTR [rip+0x404]        # 401340 <_IO_stdin_used+0x40>
  400f3b:   00 
  400f3c:   48 8d 0c 86             lea    rcx,[rsi+rax*4]
  400f40:   31 d2                   xor    edx,edx
  400f42:   49 29 c0                sub    r8,rax
  400f45:   31 c0                   xor    eax,eax
  400f47:   4c 89 c7                mov    rdi,r8
  400f4a:   48 c1 ef 03             shr    rdi,0x3
  400f4e:   66 90                   xchg   ax,ax
  400f50:   c5 f4 59 04 01          vmulps ymm0,ymm1,YMMWORD PTR [rcx+rax*1]
  400f55:   48 83 c2 01             add    rdx,0x1
  400f59:   c5 fc 29 04 01          vmovaps YMMWORD PTR [rcx+rax*1],ymm0
  400f5e:   48 83 c0 20             add    rax,0x20
  400f62:   48 39 d7                cmp    rdi,rdx
  400f65:   77 e9                   ja     400f50 <_Z6Serialv+0xd0>
  400f67:   4c 89 c1                mov    rcx,r8
  400f6a:   4c 89 ca                mov    rdx,r9
  400f6d:   48 83 e1 f8             and    rcx,0xfffffffffffffff8
  400f71:   49 8d 04 0a             lea    rax,[r10+rcx*1]
  400f75:   48 29 ca                sub    rdx,rcx
  400f78:   49 39 c8                cmp    r8,rcx
  400f7b:   0f 84 98 00 00 00       je     401019 <_Z6Serialv+0x199>
  400f81:   48 8d 0c 86             lea    rcx,[rsi+rax*4]
  400f85:   c5 fa 10 05 8f 03 00    vmovss xmm0,DWORD PTR [rip+0x38f]        # 40131c <_IO_stdin_used+0x1c>
  400f8c:   00 
  400f8d:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  400f91:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  400f95:   48 8d 48 01             lea    rcx,[rax+0x1]
  400f99:   48 83 fa 01             cmp    rdx,0x1
  400f9d:   74 7a                   je     401019 <_Z6Serialv+0x199>
  400f9f:   48 8d 0c 8e             lea    rcx,[rsi+rcx*4]
  400fa3:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  400fa7:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  400fab:   48 8d 48 02             lea    rcx,[rax+0x2]
  400faf:   48 83 fa 02             cmp    rdx,0x2
  400fb3:   74 64                   je     401019 <_Z6Serialv+0x199>
  400fb5:   48 8d 0c 8e             lea    rcx,[rsi+rcx*4]
  400fb9:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  400fbd:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  400fc1:   48 8d 48 03             lea    rcx,[rax+0x3]
  400fc5:   48 83 fa 03             cmp    rdx,0x3
  400fc9:   74 4e                   je     401019 <_Z6Serialv+0x199>
  400fcb:   48 8d 0c 8e             lea    rcx,[rsi+rcx*4]
  400fcf:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  400fd3:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  400fd7:   48 8d 48 04             lea    rcx,[rax+0x4]
  400fdb:   48 83 fa 04             cmp    rdx,0x4
  400fdf:   74 38                   je     401019 <_Z6Serialv+0x199>
  400fe1:   48 8d 0c 8e             lea    rcx,[rsi+rcx*4]
  400fe5:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  400fe9:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  400fed:   48 8d 48 05             lea    rcx,[rax+0x5]
  400ff1:   48 83 fa 05             cmp    rdx,0x5
  400ff5:   74 22                   je     401019 <_Z6Serialv+0x199>
  400ff7:   48 8d 0c 8e             lea    rcx,[rsi+rcx*4]
  400ffb:   48 83 c0 06             add    rax,0x6
  400fff:   c5 fa 59 09             vmulss xmm1,xmm0,DWORD PTR [rcx]
  401003:   c5 fa 11 09             vmovss DWORD PTR [rcx],xmm1
  401007:   48 83 fa 06             cmp    rdx,0x6
  40100b:   74 0c                   je     401019 <_Z6Serialv+0x199>
  40100d:   48 8d 04 86             lea    rax,[rsi+rax*4]
  401011:   c5 fa 59 00             vmulss xmm0,xmm0,DWORD PTR [rax]
  401015:   c5 fa 11 00             vmovss DWORD PTR [rax],xmm0
  401019:   c5 f8 77                vzeroupper 
  40101c:   c3                      ret    
  40101d:   41 ba 04 00 00 00       mov    r10d,0x4
  401023:   41 b9 fc ff ff 0f       mov    r9d,0xffffffc
  401029:   e9 00 ff ff ff          jmp    400f2e <_Z6Serialv+0xae>
  40102e:   66 90                   xchg   ax,ax
  401030:   41 b9 00 00 00 10       mov    r9d,0x10000000
  401036:   45 31 d2                xor    r10d,r10d
  401039:   e9 f0 fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  40103e:   66 90                   xchg   ax,ax
  401040:   41 b9 ff ff ff 0f       mov    r9d,0xfffffff
  401046:   41 ba 01 00 00 00       mov    r10d,0x1
  40104c:   e9 dd fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  401051:   41 ba 02 00 00 00       mov    r10d,0x2
  401057:   41 b9 fe ff ff 0f       mov    r9d,0xffffffe
  40105d:   e9 cc fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  401062:   41 ba 03 00 00 00       mov    r10d,0x3
  401068:   41 b9 fd ff ff 0f       mov    r9d,0xffffffd
  40106e:   e9 bb fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  401073:   41 ba 05 00 00 00       mov    r10d,0x5
  401079:   41 b9 fb ff ff 0f       mov    r9d,0xffffffb
  40107f:   e9 aa fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  401084:   41 ba 06 00 00 00       mov    r10d,0x6
  40108a:   41 b9 fa ff ff 0f       mov    r9d,0xffffffa
  401090:   e9 99 fe ff ff          jmp    400f2e <_Z6Serialv+0xae>
  401095:   90                      nop
  401096:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  40109d:   00 00 00 

完整代码:

#include <iostream>
#include <xmmintrin.h>
#include <immintrin.h>


using namespace std;

/**
 * The vector size
 * 268435456 -> 32*8388608 -> 2^32
 */
#define SIZE 268435456

/**
 * The vector for computations
 */
float *vector;

/**
 * Run AVX Code
 */
void AVX()  ... 


/**
 * Run Compiler optimized version
 */
void Serial()  ... 


/**
 * Create the vector
 */
void create() 
    vector = new float[SIZE];


/**
 * Fill the vector with data
 * to be used for validation
 */
void fill() 

    uint_fast32_t loop = 0;

    // Fill the vector
    for ( loop = 0  ; loop < SIZE ; loop++ )
        vector[loop] = 1;




/**
 * A validation to ensure the compiler have
 * computed all the vector data
 */
void validation() 

    // The loop variable
    unsigned long loop = 0;
    unsigned long errors = 0;
    unsigned long checks = 0;

    for ( loop = 0 ; loop < SIZE ; loop ++  ) 

        // All the vector must be 5
        if ( vector[loop] != 5 ) 
            errors ++;

            // To avoid to show too many errors
            if ( errors < 12 )
                std::cout << loop << ": " << vector[loop] << std::endl;

        

        checks ++;
    

    // The result
    std::cout << "Errors: " << errors << "\nChecks: " << checks << std::endl;





int main() 

    // Create the vector
    create();
    // Fill with data
    //fill();

    // The tests

    //Serial();
    AVX();

    /*
     * To ensure that the g++ optimization have executed the loop
     */
    //validation();


编译: g++ -O3 -mtune=native -march=native -mavx -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/Test AVX.d" -MT"src/Test\ AVX.d" -o "src/Test AVX.o" "../src/Test AVX.cpp"

【问题讨论】:

你应该展示你的整个代码,包括遍历所有元素的循环。此外,您在 AVX 版本中无效地访问内存。 f_data 只有 4 个元素(128 位),您一次加载/存储 8 个元素。 这里有点非常错误。几乎所有的汇编指令都在访问内存。那不应该发生;所有这些数据都应该注册。您是否在禁用优化的情况下进行编译? 启用优化后会是什么样子?您显示的生成代码效率非常低,正如您所期望的那样,没有优化。 您没有在启用优化的情况下进行编译。您的示例代码不会将一百万个组件乘以常数,因此它不是原始代码的简化版本。它做了一些不同的事情,当在启用优化的情况下编译时,它会在计算结果然后丢弃结果时完全编译掉。 当操作如此简单时,编译器通常能够为循环生成相等(或更好)的 avx 代码。如果优化后的代码必须能够处理所有向量大小,无论是否对齐到 16/32,它可能看起来很臃肿。最后,这个操作会被内存绑定。 【参考方案1】:

乘以 5 非常简单,您应该在下次读取数组时立即执行此操作,或者将其折叠到编写此数组的代码中。将所有这些数据从 RAM 加载到 CPU 并再次存储回来只是为了乘以 5.0 效率不高。

如果您不能将其折叠到算法的不同通道中,请尝试使用缓存阻塞(也称为循环平铺)在该数组中适合缓存的一部分上运行算法的多个步骤,然后再继续下一个缓存大小的块。


您的标量代码自动矢量化为与手动矢量化版本几乎相同的内部循环。两者都没有展开。

gcc 版本中的额外代码大小只是标量启动/清理,因此其内部循环可以使用对齐的加载/存储。 gcc 完全展开这些循环。

另请注意,您的手动矢量化代码无法处理 SIZE 不是 8 的倍数的情况。(gcc 确实会在最后处理清理,因为它不知道对齐边界的位置会的。)


clang 通常只在无法证明在编译时始终对齐的数组上使用未对齐的加载/存储。 gcc 的默认行为可能适用于在运行时实际上未对齐的大型数组,但是对于数据实际上在大多数时间在运行时对齐的情况,或者对于其中的小型数组,完全浪费了 I-cache 和分支做一堆分支和标量迭代是不值得的。


内部循环几乎相同。在您的手动矢量化版本中,gcc 设法通过f_data 优化逐个元素的复制并发出您将从_mm256_loadu_ps(&amp;vector[loop]) 获得的内容,而不是实际复制到本地然后执行矢量加载。同样的存储回vector[],你很幸运。

  # top of inner loop in the manually-vectorized version:
  400e00:   c5 f4 59 00             vmulps ymm0,ymm1,YMMWORD PTR [rax]
  400e04:   48 83 c0 20             add    rax,0x20
  400e08:   c5 fc 11 40 e0          vmovups YMMWORD PTR [rax-0x20],ymm0
  400e0d:   48 39 c2                cmp    rdx,rax
  400e10:   75 ee                   jne    400e00 <_Z3AVXv+0x20>

gcc 的内部循环使用与指针分离的循环计数器,因此它有一个额外的指令,并且它使用索引寻址模式。 vmulps ymm0,ymm1,YMMWORD PTR [rcx+rax*1] can't stay micro-fused on Haswell,因此它将作为 2 个融合域微指令发布。

  # top of gcc's inner loop:
  400f50:   c5 f4 59 04 01          vmulps ymm0,ymm1,YMMWORD PTR [rcx+rax*1]
  400f55:   48 83 c2 01             add    rdx,0x1
  400f59:   c5 fc 29 04 01          vmovaps YMMWORD PTR [rcx+rax*1],ymm0
  400f5e:   48 83 c0 20             add    rax,0x20
  400f62:   48 39 d7                cmp    rdi,rdx
  400f65:   77 e9                   ja     400f50 <_Z6Serialv+0xd0>

额外的add 指令是另一个额外的微指令。这是 6 个融合域微指令(因此最多可以每 1.5 个周期运行一次迭代,在前端成为瓶颈)。

您的手动版本只有 4 个融合域微指令,因此每个时钟可以发出 1 个。如果缓冲区在 L1D 缓存(或者可能是 L2)中很热,理论上它可以运行得那么快,并且每个时钟也受到 1 个存储的限制。


当然,因为你在一个巨大的缓冲区上运行它,你只是内存带宽的瓶颈。自动矢量化版本中的次要前端瓶颈是完全没有问题的。即使是 SSE2 版本的运行速度也几乎不会变慢。

您谈到了 16 核 Xeon。如果您希望 gcc 自动并行化以及 SIMD 矢量化,您可以使用 OpenMP。事实上,您的代码是纯单线程的。

【讨论】:

非常感谢,@Peter Corde,您的回答让我思考了很多事情,并给了我许多我不知道的细节。真的,非常感谢。关于英特尔至强,我说的是 AVX 512,可用于至强融核和至强 Skylake 微架构。

以上是关于为啥这个 AVX 代码比较慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 SSE 和 AVX 具有相同的效率?

为啥并行 SIMD/SSE/AVX 需要置换?

为啥这个 numba 代码比 numpy 代码慢 6 倍?

为啥这个 F# 代码这么慢?

我为类编写了这个汉明编码代码。为啥这么慢?

与 SSE2 相比,为啥 AVX 没有进一步提高性能?