在什么情况下AVX2收集指令比单独加载数据更快?

时间:2014-07-15 11:02:52

标签: optimization assembly vectorization avx2

我一直在研究使用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和nonvectorized程序集版本彼此非常接近。 (我还检查了gcc的汇编输出,这与我的手动编码版本非常相似。)矢量化为小型数组提供了一些好处,但对于大型数组则更慢。 (至少对我来说)最大的惊喜是使用vgatherpdp的版本太慢了。所以,我的问题是,为什么?我在这里做些蠢事吗? 有人可以提供一个示例,其中收集指令实际上只会比多次加载操作带来性能优势吗? 如果没有,实际拥有这样的指令有什么意义?

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

2 个答案:

答案 0 :(得分:11)

不幸的是,收集的加载指令并不是特别的“聪明”和#34; - 无论加载地址如何,它们似乎都会为每个元素生成一个总线周期,因此即使您碰巧有连续的元素,也显然没有用于合并负载的内部逻辑。因此,就效率而言,聚集的负载并不比N个标量负载好,只是它只使用一条指令。

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

答案 1 :(得分:6)

较新的微体系结构已使收集指令的可能性降低了。在采用Skylake微体系结构的2.00 GHz Intel Xeon Gold 6138 CPU上,我们可以为您提供基准:

$('.update')

表明收集现在可能是值得的。