为什么这个 AVX 代码比较慢?

Why is this AVX code slower?

更新: 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"

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

如果您不能将它折叠到算法的不同通道中,请尝试缓存阻塞又名循环平铺,以 运行 算法的多个步骤超过适合缓存的数组部分, 在移动到下一个缓存大小的块之前。


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

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

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


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


内部循环几乎相同。在您的手动矢量化版本中,gcc 设法通过 f_data 优化逐个元素的复制并发出您将从 _mm256_loadu_ps(&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 个融合域 uops 发出。

  # 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指令是另一个额外的uop。这是 6 个融合域 uops(因此 运行 最多每 1.5 个周期进行一次迭代,在前端出现瓶颈)。

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


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

您说的是 16 核至强。如果您希望 gcc 自动 并行化 以及 SIMD 矢量化,您可以使用 OpenMP。实际上,您的代码是纯单线程的。