c - 为什么使用 AVX2 的加速比预期的要低?

标签 c x86 intrinsics avx2

我已经使用 AVX2 的内在指令对矩阵加法的内部循环进行了向量化,我还有来自 here 的延迟表.我预计加速应该是 5 倍,因为在 1024 次迭代中几乎有 4 次延迟发生在 128 次迭代中有 6 次延迟,但加速是 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

最佳答案

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

此外,您的数字非常疯狂。 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 个融合域微指令意味着循环可以每 2c 一次迭代从循环缓冲区发出。 (不是每 1.75c)。由于我们混合使用加载、存储和 ALU 微指令,执行端口不是瓶颈,只是融合域 4 宽问题宽度。每 2c 两次加载和每 2c 一次存储只是加载和存储执行单元吞吐量的一半。

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

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


Agner Fog 的存储和加载延迟数字也有点假。他任意地将总的加载-存储往返延迟(带存储转发)分成加载和存储的延迟数。 IDK 为什么他没有列出通过指针跟踪测试测量的加载延迟(例如重复 mov (%rsi), %rsi)。这表明英特尔 SnB 系列 CPU 具有 4 个周期的加载使用延迟。

我打算给他发一封关于此事的便条,但还没抽出时间。


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

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

我的其他理论似乎也不太可能,但是您的数组地址彼此偏移正好 4096B 吗?来自 Agner Fog 的微架构 PDF:

It is not possible to read and write simultaneously from addresses that are spaced by a multiple of 4 Kbytes

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

尝试将数组彼此偏移额外的 128B 或 256B 或其他值是值得的。

关于c - 为什么使用 AVX2 的加速比预期的要低?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36247281/

相关文章:

使用 PAPI_read_counters 计算 L1 缓存未命中会产生意外结果

clang - Clang是否有类似#pragma GCC目标的内容?

c - 关于C语言中的指针

c - 汇编调用映射到错误的地址

c - C 预处理器指令的 Lex 规则

qt - x86 Qt应用程序在Mac OS 10.7(Lion)上崩溃

c++ - 矢量化 : multiply _m256i elements

c - 以下 SSE2 代码如何读取数据

c - 任何单个源 c 文件的可重用 make 文件

c - 如何从终端向 Mac 上编译的 C 程序提供输入