使用AVX2为什么加速比低于预期?

时间:2016-03-27 12:24:16

标签: c x86 intrinsics avx2

我使用AVX2的内在指令向量化了矩阵加法的内部循环,我也有here的延迟表。我预计加速应该是5的因子,因为在128次迭代中,在6次延迟的1024次迭代中发生了近4次延迟,但是加速是3的因数。所以问题是这里还有什么我不会看到。我使用gcc,c编码,内在函数,CPU是skylake 6700hq

这是内循环的c和汇编输出。

全球数据:

int __attribute__(( aligned(32))) a[MAX1][MAX2] ;
int __attribute__(( aligned(32))) b[MAX2][MAX3] ;
int __attribute__(( aligned(32))) c_result[MAX1][MAX3] ;

顺序:

for( i = 0 ; i < MAX1 ; i++)
        for(j = 0 ; j < MAX2 ; j++)
            c_result[i][j] = a[i][j] + b[i][j];

.L16:
    movl    (%r9,%rax), %edx           // latency : 2  , throughput : 0.5   number of execution unit : 4 ALU 
    addl    (%r8,%rax), %edx           // latency : dont know , throughput :    0.5     number of execution unit : 4 ALU 
    movl    %edx, c_result(%rcx,%rax)  // latency : 2 , throughput : 1  number of execution unit : 4 ALU 
    addq    $4, %rax
    cmpq    $4096, %rax
    jne .L16

AVX2:

for( i = 0 ; i < MAX1 ; i++){
   for(j = 0 ; j < MAX2 ; j += 8){
      a0_i= _mm256_add_epi32( _mm256_load_si256((__m256i *)&a[i][j]) ,  _mm256_load_si256((__m256i *)&b[i][j])); 
            _mm256_store_si256((__m256i *)&c_result[i][j], a0_i);
    }}

.L22:
    vmovdqa (%rcx,%rax), %ymm0           // latency : 3 , throughput : 0.5      number of execution unit : 4 ALU
    vpaddd  (%r8,%rax), %ymm0, %ymm0     // latency : dont know , throughput : 0.5  number of execution unit : 3 VEC-ALU
    vmovdqa %ymm0, c_result(%rdx,%rax)   // latency : 3 , throughput : 1    number of execution unit : 4 ALU
    addq    $32, %rax
    cmpq    $4096, %rax
    jne .L22

2 个答案:

答案 0 :(得分:3)

除了循环计数器之外,没有循环携带的依赖链。因此,不同循环迭代的操作可以立即进行。这意味着延迟不是瓶颈,只是吞吐量(执行单元和前端(每个时钟最多4个融合域uop))。

另外,你的号码完全是疯了。 mov加载不需要4个ALU执行单元!并且加载/存储延迟数字是错误/无意义的(参见最后一节)。

# Scalar  (serial is the wrong word.  Both versions are serial, not parallel)
.L16:
    movl    (%r9,%rax), %edx           // fused-domain uops: 1.  Unfused domain: a load port
    addl    (%r8,%rax), %edx           // fused-domain uops: 2   Unfused domain: a load port and any ALU port
    movl    %edx, c_result(%rcx,%rax)  // fused-domain uops: 2   Unfused domain: store-address and store-data ports.  port7 can't handle 2-reg addresses
    addq    $4, %rax                   // fused-domain uops: 1   unfused: any ALU
    cmpq    $4096, %rax                // fused-domain uops: 0 (fused with jcc)
    jne .L16                           // fused-domain uops: 1   unfused: port6 (predicted-taken branch)

总计:7个融合域uops意味着循环可以在每2c一次迭代的循环缓冲区中发出。 (不是每1.75c)。由于我们使用了加载,存储和ALU uop的混合,执行端口不是瓶颈,只是融合域4宽的问题宽度。每2c两个负载和每2c一个存储只是加载和存储执行单元的一半吞吐量。

请注意,2寄存器寻址模式can't micro-fuse on Intel SnB-family。对于纯负载来说,这不是一个问题,因为即使没有微融合,它们也会出现问题。

矢量循环的分析是相同的。 (vpaddd在Skylake和几乎所有其他CPU上都有1c的延迟。该表没有在padd的延迟列中列出具有内存操作数的任何内容,因为加载的延迟是与add的延迟分开。它只向一个涉及寄存器src / dest的dep链添加一个循环,只要提前知道加载地址足够远。)

Agner Fog的商店和负载延迟数字也有点虚假。他任意将总加载 - 存储往返延迟(带存储转发)分为加载和存储的延迟数。 IDK为什么没有列出由指针追逐测试(例如重复mov (%rsi), %rsi)测量的负载延迟。这表明Intel SnB系列CPU有4个周期的负载使用延迟。

我打算给他发一张关于那个的说明,但还没有找到它。

应该看到AVX2的加速比为32/4,即8x。您的问题大小仅为4096B,这对于该大小的三个数组来说足够小以适合L1缓存。 (编辑:问题是误导性的:显示的循环是嵌套循环的内部循环。看到评论:显然即使有4k阵列(不是4M),OP仍然只看到3倍的加速(与4M阵列的1.5倍相比),因此AVX版本存在某种瓶颈。)

所有3个数组都是对齐的,所以它不是缓存线交叉的 内存操作数不需要对齐(%r8)。

我的其他理论似乎也不太可能,但你的阵列地址是否相互偏移了4096B?来自Agner Fog的microarch PDF:

  

无法同时从地址读取和写入   间隔4千字节的倍数

该示例显示了一个商店然后加载,所以IDK如果真的解释它。即使内存排序硬件认为加载和存储可能是相同的地址,我也不确定为什么会阻止代码维持尽可能多的内存操作,或者为什么它会影响AVX2代码比标量码。

值得尝试通过额外的128B或256B或其他东西来抵消阵列。

答案 1 :(得分:0)

以下限制限制了两个实现的性能。首先,除了循环计数器之外,没有循环携带的依赖链,因此可以立即执行来自不同循环迭代的操作,这意味着延迟不是主要的瓶颈,如何延迟是HPC中的重要因素。由于延迟相等,因此执行单元的吞吐量对于两种实现都更有效。 IACA将标量实现的吞吐量瓶颈演示为“迭代间”,这意味着循环的连续迭代之间存在依赖关系,矢量化有助于使代码运行更快。此外,矢量化模式下的vpaddd可以在端口5,1上发布但是当端口0在第一个周期忙时,add使用执行端口1,5,6。其次,融合域前端的吞吐量可能会影响性能,但在此算法中,根据IACA结果,两次实现需要每次迭代7次uops,而HSW / SKL微架构最多可发出4次融合 - 每个时钟的域uop因此它需要内循环的每次迭代2个循环,并且这个限制违反了AVX2实现而不是标量实现。第三,算法的数据依赖性导致许多高速缓存未命中。通过减小适合L1D(第一级数据高速缓存)的矩阵的大小变为5的因子(我多久测试得到5但是IDK再次测试加速是7.3 )。