【问题标题】:How to measure speed of short piece of C/assembly code?如何测量一小段 C/汇编代码的速度?
【发布时间】:2017-06-18 19:23:51
【问题描述】:

考虑这个 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

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

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 

我对哪个更快感兴趣,因此测量运行时间会很棒。

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

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

【问题讨论】:

标签: c linux performance assembly


【解决方案1】:

您需要一个可以多次调用此函数的测试工具。

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

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);
    }
}

【讨论】:

  • 优化器不会消除循环吗?
  • 对于 gcc 你可以在测试函数上使用__attribute__ ((optimize(0))) 来防止它被优化。或者,您将测试函数放在使用不同优化设置编译的不同 .c 文件中。
  • 如果您关闭优化,除非我误解了,否则您不会得到问题中的程序集。您能否明确说明其他解决方案的工作原理?
  • @eleanora 您将测试工具放入文件harness.c,并将要测试的函数放入文件function.c。如果您愿意,可以使用-O3 编译这两个文件。然后将结果链接在一起而不进行优化。这样,优化器就不会看到在测试工具中多次调用f(x) 是没有意义的,也不会优化掉循环。但是,它会尽可能优化f()
  • @cmaster 听起来不错。您能否添加一些命令行示例的答案以便我接受?
【解决方案2】:

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

确保您处于 turbo 模式(或在 BIOS 设置中禁用所有频率缩放)并且矢量单元在计时之前“唤醒”(对于 AVX 代码),所以做一些热身。您打算计时的相同代码可以做到这一点。

要非常小心,故意选择使迭代依赖(测量延迟)或独立(测量吞吐量),不要只是随机做一些事情 - 你会有一个你不知道的测量是延迟还是吞吐量。也不要让编译器对其进行优化,从而消除对您正在测量的事物的部分或全部调用,这样您最终会测量 nothing

您可以将rdtscp 用于计时本身,或者使用不太精确的东西 - 精度越低,您的计时循环需要的时间就越长。您可以从绘图的外观大致判断精度,如果它看起来高度离散,并带有一些所有内容都排成一列的“箱”,请使用更多迭代(或更好的时间测量)。

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

FWIW ICC asm 看起来更快,GCC 正在做大量的标量数学运算。

【讨论】:

  • 如果你只是重复循环,如何避免优化器消除循环?
  • @eleanora 您可以将函数放在不同的编译单元中(然后不要使用 LTO)(然后必须在循环中调用它,因为优化器必须假设函数可能会做某事有趣),或者只是在汇编中编写循环并确保它完全符合您的要求
  • 你能展示这些想法所需的代码吗?如果可以在程序集中添加看起来像最佳解决方案但我不知道如何混合 C 和程序集的循环。
  • @eleanora 我可以展示一些示例,但这取决于平台详细信息(和汇编程序)。不过,将 C 和汇编结合起来并不难,只需将它们编译/汇编成目标文件并像往常一样使用链接器将它们链接在一起。从汇编中调用 C 函数需要正确的名称(可能您需要在函数名称前加下划线)和某种形式的 extern 声明(汇编器之间的语法不同)
  • 谢谢。我在 linux 上,很高兴使用对您来说最方便的任何工具。例如,我认为我粘贴的程序集应该使用gas 进行编译/组装。
【解决方案3】:

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

  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 算术等快速操作,则应运行比较测试,将函数定义为

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

【讨论】:

  • 如何编译完全优化但链接不优化?
  • 编译时使用gcc -O3,链接时使用gcc。链接器通常只会将编译期间创建的符号视为黑盒。但是,afaik,有一些链接时优化可以显式打开。只是不要将这些指定给链接器命令,你应该没问题。
猜你喜欢
  • 2010-11-15
  • 2018-05-07
  • 1970-01-01
  • 1970-01-01
  • 2011-08-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多