在啥情况下,AVX2 收集指令会比单独加载数据更快?

Posted

技术标签:

【中文标题】在啥情况下,AVX2 收集指令会比单独加载数据更快?【英文标题】:In what situation would the AVX2 gather instructions be faster than individually loading the data?在什么情况下,AVX2 收集指令会比单独加载数据更快? 【发布时间】:2014-07-15 11:02:52 【问题描述】:

我一直在研究 AVX2 指令集的新收集指令的使用。具体来说,我决定对一个简单的问题进行基准测试,其中一个浮点数组被置换并添加到另一个。在c中,这可以实现为

void vectortest(double * a,double * b,unsigned int * ind,unsigned int N)

    int i;
    for(i=0;i<N;++i)
    
        a[i]+=b[ind[i]];
    

我用 g++ -O3 -march=native 编译这个函数。现在,我以三种方式在汇编中实现它。为简单起见,我假设数组 N 的长度可以被 4 整除。简单的非向量化实现:

align 4
global vectortest_asm
vectortest_asm:
        ;;  double * a = rdi                                                                                                                                                                                                                                   
        ;;  double * b = rsi                                                                                                                                                                                                                                   
        ;;  unsigned int * ind = rdx                                                                                                                                                                                                                           
        ;;  unsigned int N = rcx                                                                                                                                                                                                                               

        push rax
        xor rax,rax

loop:   sub rcx, 1
        mov eax, [rdx+rcx*4]    ;eax = ind[rcx]                                                                                                                                                                                                                
        vmovq xmm0, [rdi+rcx*8]         ;xmm0 = a[rcx]                                                                                                                                                                                                         
        vaddsd xmm0, [rsi+rax*8]        ;xmm1 += b[rax] ( and b[rax] = b[eax] = b[ind[rcx]])                                                                                                                                                                   
        vmovq [rdi+rcx*8], xmm0
        cmp rcx, 0
        jne loop

        pop rax

        ret

没有收集指令的循环向量化:

loop:   sub rcx, 4

        mov eax,[rdx+rcx*4]    ;first load the values from array b to xmm1-xmm4
        vmovq xmm1,[rsi+rax*8]
        mov eax,[rdx+rcx*4+4]
        vmovq xmm2,[rsi+rax*8]

        mov eax,[rdx+rcx*4+8]
        vmovq xmm3,[rsi+rax*8]
        mov eax,[rdx+rcx*4+12]
        vmovq xmm4,[rsi+rax*8]

        vmovlhps xmm1,xmm2     ;now collect them all to ymm1
        vmovlhps xmm3,xmm4
        vinsertf128 ymm1,ymm1,xmm3,1

        vaddpd ymm1, ymm1, [rdi+rcx*8]
        vmovupd [rdi+rcx*8], ymm1

        cmp rcx, 0
        jne loop

最后,一个使用 vgatherdpd 的实现:

loop:   sub rcx, 4               
        vmovdqu xmm2,[rdx+4*rcx]           ;load the offsets from array ind to xmm2
        vpcmpeqw ymm3,ymm3                 ;set ymm3 to all ones, since it acts as the mask in vgatherdpd                                                                                                                                                                 
        vgatherdpd ymm1,[rsi+8*xmm2],ymm3  ;now gather the elements from array b to ymm1

        vaddpd ymm1, ymm1, [rdi+rcx*8]
        vmovupd [rdi+rcx*8], ymm1

        cmp rcx, 0
        jne loop

我在一台装有 Haswell cpu (Xeon E3-1245 v3) 的机器上对这些功能进行了基准测试。一些典型的结果是(以秒为单位的时间):

Array length 100, function called 100000000 times.
Gcc version: 6.67439
Nonvectorized assembly implementation: 6.64713
Vectorized without gather: 4.88616
Vectorized with gather: 9.32949

Array length 1000, function called 10000000 times.
Gcc version: 5.48479
Nonvectorized assembly implementation: 5.56681
Vectorized without gather: 4.70103
Vectorized with gather: 8.94149

Array length 10000, function called 1000000 times.
Gcc version: 7.35433
Nonvectorized assembly implementation: 7.66528
Vectorized without gather: 7.92428
Vectorized with gather: 8.873

gcc 和非向量化汇编版本非常接近。 (我还检查了 gcc 的汇编输出,这与我的手动编码版本非常相似。)矢量化对小型数组有一些好处,但对大型数组来说速度较慢。最大的惊喜(至少对我来说)是使用 vgatherpdp 的版本太慢了。所以,我的问题是,为什么?我在这里做傻事吗? 有人可以提供一个示例,说明收集指令实际上比仅执行多个加载操作会带来性能优势吗?如果不是,那么实际拥有这样的指令有什么意义?

测试代码(包含用于 g++ 和 nasm 的 makefile)可在 https://github.com/vanhala/vectortest.git 获得,以防有人想尝试。

【问题讨论】:

好吧,您的手工编码函数更快也就不足为奇了,毕竟 C 编译器必须生成正确的代码。您的循环没有规定数组长度不是向量化大小的倍数,甚至不检查计数是否为零...... @EOF 是的,但这不是重点。该基准测试的主要目的是比较收集的加载指令与使用标量加载实现相同事物的效率。编译器生成的版本只是为了确保时间在正确的范围内,即检查我在手工编码的版本中没有做任何完全愚蠢的事情。 【参考方案1】:

不幸的是,收集到的加载指令并不是特别“智能”——它们似乎为每个元素生成一个总线周期,而不管加载地址如何,所以即使你碰巧有连续的元素,显然也没有用于合并加载的内部逻辑.所以就效率而言,一个聚集的负载并不比 N 个标量负载好,只是它只使用一条指令。

收集指令的唯一真正好处是当您无论如何都在实现 SIMD 代码时,您需要加载非连续数据,然后您将对其应用进一步的 SIMD 操作。在这种情况下,SIMD 收集的加载指令将比通常由例如生成的一堆标量代码更有效。 _mm256_set_xxx()(或一堆连续的加载和置换等,取决于实际的访问模式)。

【讨论】:

我不确定我是否理解你的后一点。在上面的示例中,我从数组 b 加载非连续数据,然后将一些 SIMD 指令应用于该数据。在这种情况下,用一堆标量 mov 替换收集指令会产生更快的代码。您能否提供一个指向实际基准或示例的指针,其中收集的负载不会比多个标量负载慢?或者你的意思是编译器使用收集指令生成代码更容易,即使它更慢? 上面评论最后一句中的“慢”是指“比使用标量指令手工制作的加载代码慢”。 @infinitesimal 此外,收集可以是有条件的。顺便说一句,您可能想尝试将 vpcmpeqw 移出循环,将所有 1 位存储在一个备用寄存器中,您每次只需将其复制到掩码中。 @Jester 是的,额外的逻辑可能是它看起来很慢的原因之一。移动 vpcmpeqw 不会对执行时间造成可测量的差异。 更新的微架构如何改变?【参考方案2】:

较新的微架构已将可能性转向收集指令。在配备 Skylake 微架构的 Intel Xeon Gold 6138 CPU @ 2.00 GHz 上,我们为您提供基准测试:

9.383e+09 8.86e+08 2.777e+09 6.915e+09 7.793e+09 8.335e+09 5.386e+09 4.92e+08 6.649e+09 1.421e+09 2.362e+09 2.7e+07 8.69e+09 5.9e+07 7.763e+09 3.926e+09 5.4e+08 3.426e+09 9.172e+09 5.736e+09 
9.383e+09 8.86e+08 2.777e+09 6.915e+09 7.793e+09 8.335e+09 5.386e+09 4.92e+08 6.649e+09 1.421e+09 2.362e+09 2.7e+07 8.69e+09 5.9e+07 7.763e+09 3.926e+09 5.4e+08 3.426e+09 9.172e+09 5.736e+09 
9.383e+09 8.86e+08 2.777e+09 6.915e+09 7.793e+09 8.335e+09 5.386e+09 4.92e+08 6.649e+09 1.421e+09 2.362e+09 2.7e+07 8.69e+09 5.9e+07 7.763e+09 3.926e+09 5.4e+08 3.426e+09 9.172e+09 5.736e+09 
9.383e+09 8.86e+08 2.777e+09 6.915e+09 7.793e+09 8.335e+09 5.386e+09 4.92e+08 6.649e+09 1.421e+09 2.362e+09 2.7e+07 8.69e+09 5.9e+07 7.763e+09 3.926e+09 5.4e+08 3.426e+09 9.172e+09 5.736e+09 
Array length 10000, function called 1000000 times.
Gcc version: 6.32353
Nonvectorized assembly implementation: 6.36922
Vectorized without gather: 5.53553
Vectorized with gather: 4.50673

表明现在的聚会可能是值得的。

【讨论】:

以上是关于在啥情况下,AVX2 收集指令会比单独加载数据更快?的主要内容,如果未能解决你的问题,请参考以下文章

memcpy在啥情况下会失败

最快的步幅 2 聚集

加载指令与 AVX 中的 AVX2 __m256i const* mem_addr [关闭]

java中几种Map在啥情况下使用?

AppDomain.DoCallback()在啥情况下会失败?

SQL SERVER,使用(TABLOCKX)选择查询会比使用(NOLOCK)更快,反之亦然?