单CPU /双精度SpMV在CPU上的性能

时间:2016-04-11 21:46:09

标签: c++ performance sparse-matrix precision

由于算术强度非常低,稀疏矩阵向量积是一种存储器绑定操作。由于浮点存储格式每非零需要4 + 4 = 8个字节,而双精度(值和列索引)需要4 + 8 = 12个字节,因此在切换到浮点数时,应该可以预期执行速度提高约33%。我构建了一个基准,它组装了一个1000000x1000000矩阵,每行有200个非零,然后从20次乘法中取最小值。 github上的源代码here

结果大致与我的预期相符。当我在我的英特尔酷睿i7-2620M上运行基准测试时,我发现执行速度提高了30%。在spec的21.3 GB / s中,带宽从大约19.0 GB / s(双倍)下降到大约18.0 GB / s(浮点数)可以看出很小的差异。

现在,由于矩阵的数据几乎比矢量的数据大1000,人们可以预期,对于只有矩阵是单精度的情况,但是矢量保持为的情况,也应该获得更快的性能。双打。我继续尝试这个,然后确保使用较低的精度进行计算。但是,当我运行它时,有效带宽使用率突然降至约14.4 GB / s,执行速度仅比完整双版本快12%。怎么能理解这个?

我正在使用Ubuntu 14.04和GCC 4.9.3。

Run times:

// double(mat)-double(vec)
Wall time: 0.127577 s
Bandwidth: 18.968 GB/s
Compute:   3.12736 Gflop/s

// float(mat)-float(vec)
Wall time: 0.089386 s
Bandwidth: 18.0333 GB/s
Compute:   4.46356 Gflop/s

// float(mat)-double(vec)
Wall time: 0.112134 s
Bandwidth: 14.4463 GB/s
Compute:   3.55807 Gflop/s

更新

请参阅下面的Peter Cordes的答案。简而言之,从double到float转换的循环迭代之间的依赖性是造成开销的原因。通过展开循环(参见github上的unroll-loop分支),float-double和float-float版本都可以重新获得全带宽使用!

New run times:

// float(mat)-float(vec)
Wall time: 0.084455 s
Bandwidth: 19.0861 GB/s
Compute:   4.72417 Gflop/s

// float(mat)-double(vec)
Wall time: 0.0865598 s
Bandwidth: 18.7145 GB/s
Compute:   4.6093 Gflop/s

1 个答案:

答案 0 :(得分:2)

必须快速转换的双浮动循环不会发出同样快的问题。通过一些循环展开,gcc可能会做得更好。

你的i7-2620M is a dual-core with hyperthreading。当瓶颈是CPU uop吞吐量而不是分支错误预测,缓存未命中,甚至只是长延迟链时,超线程并不会有所帮助。仅通过标量操作来饱和内存带宽并不容易。

Godbolt Compiler Explorer上的代码的asm输出:gcc 5.3产生相同的内循环BTW,因此在这种情况下你不会因使用旧的gcc版本而丢失很多。

双倍版内循环(gcc 4.9.3 -O3 -march=sandybridge -fopenmp):

## inner loop of <double,double>mult() with fused-domain uop counts
.L7:
    mov     edx, eax  # 1 uop
    add     eax, 1    # 1 uop
    mov     ecx, DWORD PTR [r9+rdx*4] # 1 uop
    vmovsd  xmm0, QWORD PTR [r10+rdx*8] # 1 uop
    vmulsd  xmm0, xmm0, QWORD PTR [r8+rcx*8]    # 2 uops
    vaddsd  xmm1, xmm1, xmm0    # 1 uop
    cmp     eax, esi  #  (macro-fused)
    jne     .L7       #  1 uop

总计:8个融合域uops,每两个时钟可以发出一次。它也可以快速执行:三个uop是负载,SnB每2个时钟可以执行4个负载。剩下5个ALU uops(因为SnB不能消除重命名阶段中的reg-reg移动,这是IvB引入的)。无论如何,单个执行端口没有明显的瓶颈。 SnB的三个ALU端口每两个周期最多可处理六个ALU uop。

没有微融合because of using two-register addressing modes

双浮版内循环:

## inner loop of <double,float>mult() with fused-domain uop counts
.L7:
    mov     edx, eax  # 1 uop
    vxorpd  xmm0, xmm0, xmm0    # 1 uop (no execution unit needed).
    add     eax, 1    # 1 uop
    vcvtss2sd       xmm0, xmm0, DWORD PTR [r9+rdx*4]      # 2 uops
    mov     edx, DWORD PTR [r8+rdx*4] # 1 uop
    vmulsd  xmm0, xmm0, QWORD PTR [rsi+rdx*8]   # 2 uops
    vaddsd  xmm1, xmm1, xmm0    # 1 uop
    cmp     eax, ecx  # (macro-fused)
    jne     .L7       # 1 uop

gcc使用xorpd来打破循环携带的依赖链。 cvtss2sd对xmm0的旧值具有错误的依赖性,因为它的设计很糟糕,并且没有将寄存器的上半部分归零。 (movsd用作加载时为零,但在用作注册移动时则不行。在这种情况下,除非要合并,否则请使用movaps。)

因此,10个融合域uops:每三个时钟可以发出一次迭代。我认为这是唯一的瓶颈,因为它只需要一个需要执行端口的额外ALU uop。 (SnB handles zeroing-idioms in the rename stage, so xorpd doesn't need one)。 cvtss2sd是一个2 uop指令,即使gcc使用单寄存器寻址模式,显然也不能进行微熔丝。它的吞吐量为每时钟一个。 (在Haswell上,当与寄存器src和dest一起使用时,它是一个2 uop指令,根据Agner Fog's testing,在Skylake上,吞吐量减少到每2个时钟一个。)仍然不会但是,成为Skylake这个循环的瓶颈。它仍然是Haswell / Skylake的10个融合域uops,而且仍然是瓶颈。

-funroll-loops应该有助于gcc 4.9.3

gcc做得不错,代码如

    mov     edx, DWORD PTR [rsi+r14*4]        # D.56355, *_40
    lea     r14d, [rax+2]     # D.56355,
    vcvtss2sd       xmm6, xmm4, DWORD PTR [r8+r14*4]      # D.56358, D.56358, *_36
    vmulsd  xmm2, xmm1, QWORD PTR [rcx+rdx*8]   # D.56358, D.56358, *_45
    vaddsd  xmm14, xmm0, xmm13  # tmp, tmp, D.56358
    vxorpd  xmm1, xmm1, xmm1    # D.56358

    mov     edx, DWORD PTR [rsi+r14*4]        # D.56355, *_40
    lea     r14d, [rax+3]     # D.56355,
    vcvtss2sd       xmm10, xmm9, DWORD PTR [r8+r14*4]     # D.56358, D.56358, *_36
    vmulsd  xmm7, xmm6, QWORD PTR [rcx+rdx*8]   # D.56358, D.56358, *_45
    vaddsd  xmm3, xmm14, xmm2   # tmp, tmp, D.56358
    vxorpd  xmm6, xmm6, xmm6    # D.56358

没有循环开销,每个元素的工作量下降到8个融合域uops,并且它不是一个微小的循环,每3个循环只发出2个uop(因为10不是&ta;多个4)。

它可以通过使用置换保存lea指令,例如[r8+rax*4 + 12]。 IDK为什么选择不这样做。

甚至-ffast-math都没有让它进行矢量化。可能没有意义,因为从稀疏矩阵进行聚集会超过从非稀疏向量中加载4或8个连续值的好处。 (来自存储器的insertps是一个2-uop指令,即使使用单寄存器寻址模式也不能进行微熔丝。)

在Broadwell或Skylake上,vgatherdps可能足够快,可以加快速度。可能是Skylake的一次大加速。 (可以收集8个单精度浮点数,每5个时钟的吞吐量为8个浮点数。或者vgatherqpd可以收集4个双精度浮点数,每4个时钟的吞吐量为4个双精度浮点数。这为您设置256b矢量FMA。