如何测量短片C /汇编代码的速度?

时间:2017-02-01 21:28:03

标签: c linux performance assembly

考虑这个C代码:

#include <complex.h>
complex float f(complex float x[]) {
  complex float p = 1.0;
  for (int i = 0; i < 32; i++)
    p += x[i];
  return p;
}

使用-O3 -march=core-avx2运行的英特尔C编译器提供:

f:
        vmovups   ymm1, YMMWORD PTR [rdi]                       #5.10
        vmovups   ymm2, YMMWORD PTR [64+rdi]                    #5.10
        vmovups   ymm5, YMMWORD PTR [128+rdi]                   #5.10
        vmovups   ymm6, YMMWORD PTR [192+rdi]                   #5.10
        vmovsd    xmm0, QWORD PTR p.152.0.0.1[rip]              #3.19
        vaddps    ymm3, ymm1, YMMWORD PTR [32+rdi]              #3.19
        vaddps    ymm4, ymm2, YMMWORD PTR [96+rdi]              #3.19
        vaddps    ymm7, ymm5, YMMWORD PTR [160+rdi]             #3.19
        vaddps    ymm8, ymm6, YMMWORD PTR [224+rdi]             #3.19
        vaddps    ymm9, ymm3, ymm4                              #3.19
        vaddps    ymm10, ymm7, ymm8                             #3.19
        vaddps    ymm11, ymm9, ymm10                            #3.19
        vextractf128 xmm12, ymm11, 1                            #3.19
        vaddps    xmm13, xmm11, xmm12                           #3.19
        vmovhlps  xmm14, xmm13, xmm13                           #3.19
        vaddps    xmm15, xmm13, xmm14                           #3.19
        vaddps    xmm0, xmm15, xmm0                             #3.19
        vzeroupper                                              #6.10
        ret                                                     #6.10

-O3 -march=core-avx2 -ffast-math的gcc版本7(快照)给出:

f:
        lea     r10, [rsp+8]
        and     rsp, -32
        push    QWORD PTR [r10-8]
        push    rbp
        mov     rbp, rsp
        push    r10
        vmovups ymm0, YMMWORD PTR [rdi+64]
        vmovaps ymm1, YMMWORD PTR .LC0[rip]
        vaddps  ymm0, ymm0, YMMWORD PTR [rdi+32]
        vaddps  ymm1, ymm1, YMMWORD PTR [rdi]
        vaddps  ymm0, ymm0, ymm1
        vmovups ymm1, YMMWORD PTR [rdi+128]
        vaddps  ymm1, ymm1, YMMWORD PTR [rdi+96]
        vaddps  ymm0, ymm0, ymm1
        vmovups ymm1, YMMWORD PTR [rdi+192]
        vaddps  ymm1, ymm1, YMMWORD PTR [rdi+160]
        vaddps  ymm0, ymm0, ymm1
        vaddps  ymm0, ymm0, YMMWORD PTR [rdi+224]
        vunpckhps       xmm3, xmm0, xmm0
        vshufps xmm2, xmm0, xmm0, 255
        vshufps xmm1, xmm0, xmm0, 85
        vaddss  xmm1, xmm2, xmm1
        vaddss  xmm3, xmm3, xmm0
        vextractf128    xmm0, ymm0, 0x1
        vunpckhps       xmm4, xmm0, xmm0
        vshufps xmm2, xmm0, xmm0, 85
        vaddss  xmm4, xmm4, xmm0
        vshufps xmm0, xmm0, xmm0, 255
        vaddss  xmm0, xmm2, xmm0
        vaddss  xmm3, xmm3, xmm4
        vaddss  xmm1, xmm1, xmm0
        vmovss  DWORD PTR [rbp-24], xmm3
        vmovss  DWORD PTR [rbp-20], xmm1
        vzeroupper
        vmovq   xmm0, QWORD PTR [rbp-24]
        pop     r10
        pop     rbp
        lea     rsp, [r10-8]
        ret 

我感兴趣哪一个更快,所以测量运行时间会很棒。

但是,我不知道如何衡量花费很少时间的代码的运行时间。

  

哪种代码更快,如何可靠地测量它?

3 个答案:

答案 0 :(得分:4)

您需要一个能够多次调用此功能的测试工具。

这将使运行时达到一个非平凡的水平,并将平均由操作系统调度引起的任何差异。

void test_f() 
{
    complex float x[32] = { 1+2i, 2+3i };    // add as many as needed. 
                                             // here i is a special
                                             // constant for complex numbers
    int i;
    for (i=0; i<10000000; i++) {
        f(x);
    }
}

答案 1 :(得分:1)

经常重复它需要足够长的时间。太短使得它更容易受到微小的时序怪异(完全测量时间的开销,指令跳过时间读取指令进入或退出有效定时区域,这要归功于OoOE,无论如何),太长了......不会真的很重要,除非你是纯粹主义者。您通常可以猜测实际时间应该是什么,由于中断等原因,测得的时间会略高一些,但是在调整时钟速度后,您应该得到一个“接近”合理值的结果(对于延迟测量,您应该例如获得整数个周期)。做多次运行并绘制它,忽略奇怪的异常值,特别是在顶部。

确保您处于turbo模式(或禁用BIOS设置中的所有频率调整),并且在计时之前矢量单元处于“唤醒”状态(对于AVX代码),因此请进行一些预热。你想要的时间相同的代码可以做到这一点。

要非常小心,你有意选择依赖迭代(测量延迟)或独立(测量吞吐量),不要随意做一些事情 - 你有一个你不知道的测量是延迟或吞吐量。另外,不要让编译器对你正在测量的东西的部分或全部调用进行优化,以便最终测量 nothing

你可以使用rdtscp作为时间本身,或者不太精确的东西 - 定时循环需要的时间越长,精度越低。您可以粗略地分辨出绘图的精确度,如果它看起来非常离散,只有几个“分类”,所有内容都排成一行,使用更多的迭代(或更好的时间测量)。

如果你打算在特定的缓存条件下测量它会变得更加棘手,因为设置该状态也需要时间,所以它会变成一个“猜测开销”的游戏(这很难准确测量)。

FWIW ICC asm看起来更快,GCC正在做很多标量数学。

答案 2 :(得分:1)

每当您想要执行性能测量时,您需要执行几个步骤:

  1. 重复操作以进行足够频繁的测量,以达到可以准确测量的时间。通常一到十秒就足够了。几乎在所有情况下都可以通过简单的循环来实现。

  2. 由于未使用的结果,阻止优化器完全优化重复或完全优化测量的操作。

    有几种可能的方法:

    • 在每次迭代中修改输入并实际使用所有结果。在您的情况下,这可能看起来像这样:

      complex float accu = 0+0i;
      for(int i = 0; i < 100000000; i++) {
          x[i%32] += 42+3i;   //different input on each pass
          accu += f(x);
      }
      printComplex(accu);    //this depends on the output of all passes
      

      请注意,这种方法有点难以使用,因为它很容易忽视优化的可能性并且无法防范它。您的优化器可能会理解过于简单的输入修改的影响,并且无论如何都要编译f()的重要部分,所以要小心。这样做的好处是它允许优化器内联函数,避免函数调用开销。

    • 拆分为独立的编译单元:将重复循环放入一个.c文件中,并将要测试的函数放入另一个.c文件中。使用完全优化单独编译,并且无需优化即可链接。

      这样,优化器在编译重复循环时无法查看您的函数,因此无法知道它是否具有副作用。因此,它无法优化重复。同样,当它编译要测试的函数时,它不知道它的结果没有被使用,因此不能完全优化它的身体。链接器然后只连接部件。

      这样做的缺点是,您无法从测量中获得函数调用开销。这可能需要沿着点3的线进行参考测量。

    • 使用某些编译器指令(如#pragma__attribute__(()))来避免有害的优化。我从来没有用过这个,因为上述两种方法中的任何一种在所有情况下都很好用,但它肯定是一种选择。但是,细节是编译器特定的。

  3. 评估测量本身的开销。如果您正在测量I / O调用等高延迟操作,您可以跳过此操作,但如果您正在测量像示例中的CPU算术这样的快速内容,则应运行比较测试,将函数定义为< / p>

    complex float f(complex float x[]) {
        complex float p = 1.0;
        return p;
    }