-1

我想用 rdtsc 计时函数调用。所以我用以下两种方式测量它。

  1. 循环调用它。聚合循环内的每个 rdtsc 差异并除以调用次数。(假设这是N)
  2. 循环调用它。获取循环本身的rdtsc差异并除以N。

但我看到了一些不一致的行为。

  1. 当我增加 N 时,方法 1 和 2 中的时间都会相当单调地减少。对于方法 2,它可以分摊循环控制开销是可以理解的。但我不确定方法 1 的情况如何。
  2. 实际上,对于方法 2,每次增加 N 时,我得到的 N=1 的值似乎每次都除以新的 N。检查 gdb 反汇编让我意识到这是 -O2 的一些编译器优化,在第二种情况下跳过了循环。所以我用-O0重试了,gdb反汇编显示了第二种情况的实际循环。

代码如下。

    #include <stdio.h>
    #include <inttypes.h>
    #include <stdlib.h>

    typedef unsigned long long ticks;

    static __inline__ ticks getticks(void) {
      unsigned a, d; 
      asm volatile("rdtsc" : "=a" (a), "=d" (d)); 
      return ((ticks)a) | (((ticks)d) << 32); 
    }

    __attribute__ ((noinline))
    void bar() {

    }

    int main(int argc, char** argv) {

       long long N = 1000000; 
       N = atoi(argv[1]);
       int i;
       long long bar_total = 0;

       ticks start = 0, end = 0;

       for (i = 0; i < N; i++) {
         start = getticks();
         bar();
         end = getticks();
         bar_total += (end - start);
       } 

       fprintf(stdout, "Total invocations : %lld\n", N);
       fprintf(stdout, "[regular] bar overhead : %lf\n", ((double)bar_total/  N));

      start = getticks();
      for (i = 0; i < N; i++) {
        bar();
      } 
      end = getticks();

      bar_total = (end - start);

      fprintf(stdout, "[Loop] bar overhead : %lf\n", ((double)bar_total/ N));

      return 0;

     }

知道这里发生了什么吗?如果需要,我也可以将 gdb 反汇编。我使用了来自http://dasher.wustl.edu/tinker/distribution/fftw/kernel/cycle.h的 rdtsc 实现

编辑: 我将不得不撤回我的第二个声明,即在 -O0 处,在第二种情况下,时间与 N 成正比下降。我想这是我在构建过程中犯的一些错误,导致一些旧版本持续存在。无论如何它仍然与方法1的数字一起下降。这里是不同N值的一些数字。

taskset -c 2 ./example.exe 1
Total invocations : 1
[regular] bar overhead : 108.000000
[Loop] bar overhead : 138.000000

taskset -c 2 ./example.exe 10
Total invocations : 10
[regular] bar overhead : 52.900000
[Loop] bar overhead : 40.700000

taskset -c 2 ./example.exe 100
Total invocations : 100
[regular] bar overhead : 46.780000
[Loop] bar overhead : 15.570000

taskset -c 2 ./example.exe 1000
Total invocations : 1000
[regular] bar overhead : 46.069000
[Loop] bar overhead : 13.669000

taskset -c 2 ./example.exe 100000
Total invocations : 10000
[regular] bar overhead : 46.010100
[Loop] bar overhead : 13.444900

taskset -c 2 ./example.exe 100000000
Total invocations : 100000000
[regular] bar overhead : 26.970272
[Loop] bar overhead : 5.201252

taskset -c 2 ./example.exe 1000000000
Total invocations : 1000000000
[regular] bar overhead : 18.853279
[Loop] bar overhead : 5.218234

taskset -c 2 ./example.exe 10000000000
Total invocations : 1410065408
[regular] bar overhead : 18.540719
[Loop] bar overhead : 5.216395

我现在看到了两种新行为。

  1. 方法 1 的收敛速度比方法 2 慢。但我仍然对为什么不同 N 设置的值存在如此大的差异感到困惑。也许我在这里犯了一些我目前看不到的基本错误。
  2. 方法 1 的值实际上比方法 2 大一些。我预计它与方法 2 的值持平或略小,因为它不包含循环控制开销。

问题

总而言之,我的问题是

  1. 为什么增加 N 时两种方法给出的值变化如此之大?特别适用于不考虑循环控制开销的方法 1。

  2. 当第一种方法在计算中排除循环控制开销时,为什么第二种方法的结果小于第一种方法的结果?

编辑 2

关于建议的 rdtscp 解决方案。

由于对内联汇编不熟悉,我做了以下事情。

static __inline__ ticks getstart(void) {
  unsigned cycles_high = 0, cycles_low = 0; 
  asm volatile ("CPUID\n\t"
             "RDTSC\n\t"
             "mov %%edx, %0\n\t"
             "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
             "%rax", "%rbx", "%rcx", "%rdx");
  return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
}

static __inline__ ticks getend(void) {
  unsigned cycles_high = 0, cycles_low = 0; 
  asm volatile("RDTSCP\n\t"
         "mov %%edx, %0\n\t"
          "mov %%eax, %1\n\t"
           "CPUID\n\t": "=r" (cycles_high), "=r" (cycles_low)::
           "%rax", "%rbx", "%rcx", "%rdx");
  return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
}

并在函数调用之前和之后使用上述方法。但现在我得到如下无意义的结果。

Total invocations : 1000000
[regular] bar overhead : 304743228324.708374
[Loop] bar overhead : 33145641307.734016

有什么问题?我想将它们分解为内联方法,因为我看到在多个地方使用它。

A. 评论中的解决方案。

4

2 回答 2

2

您使用普通rdtsc指令,这可能无法在乱序 CPU(如 Xeon 和 Cores)上正常工作。您应该添加一些序列化指令或切换到rdtscp指令

http://en.wikipedia.org/wiki/Time_Stamp_Counter

从 Pentium Pro 开始,英特尔处理器支持乱序执行,其中指令不一定按照它们在可执行文件中出现的顺序执行。这可能会导致 RDTSC 的执行晚于预期,从而产生误导性的循环计数。 [3] 可以通过执行序列化指令(例如 CPUID)来解决此问题,以强制每个前面的指令在允许程序继续之前完成,或者通过使用 RDTSCP 指令(RDTSC 指令的序列化变体)来解决。

Intel 有最近使用 rdtsc/rdtscp 的手册 - How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures (ia-32-ia-64-benchmark-code-execution-paper.pdf, 324264-001 , 2010)。他们推荐 cpuid+rdtsc 用于启动和 rdtscp 用于结束计时器:

第 0 节中提出的问题的解决方案是在RDTPSCP和 两条指令之后添加一条 CPUID 指令(将和mov的值存储在内存中)。实现如下:edxeax

asm volatile ("CPUID\n\t"
 "RDTSC\n\t"
 "mov %%edx, %0\n\t"
 "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
/***********************************/
/*call the function to measure here*/
/***********************************/
asm volatile("RDTSCP\n\t"
 "mov %%edx, %0\n\t"
 "mov %%eax, %1\n\t"
 "CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "%rbx", "%rcx", "%rdx");

start = ( ((uint64_t)cycles_high << 32) | cycles_low );
end = ( ((uint64_t)cycles_high1 << 32) | cycles_low1 );

在上面的代码中,第一次CPUID调用实现了一个屏障,以避免指令上方和下方的指令乱序执行RDTSC。然而,这个调用不会影响测量,因为它出现在 之前 RDTSC(即,在读取时间戳寄存器之前)。然后第一个RDTSC读取时间戳寄存器并将值存储在内存中。然后我们要测量的代码被执行。如果代码是对函数的调用,建议将此类函数声明为“<code>inline”,这样从汇编的角度来看,调用函数本身不会产生开销。该RDTSCP指令第二次读取时间戳寄存器,并保证我们要测量的所有代码的执行完成。

你的例子不是很正确;您尝试测量空函数bar(),但它太短以至于您在方法 1 ( for() { rdtsc; bar(); rdtsc)) 中测量 rdtsc 开销。根据 Agner Fog 的 haswell 表 - http://www.agner.org/optimize/instruction_tables.pdf第 191 页(长表“Intel Haswell 指令时序和 μop 故障列表”,最后) RDTSC有 15 uops(不可能融合)和 24 个滴答声的延迟;RDTSCP(对于较旧的微架构,Sandy Bridge 的延迟时间为 23 微指令和 36 滴答声,而 rdtsc 的延迟时间为 21 微指令和 28 滴答声)。因此,您不能使用普通的 rdtsc(或 rdtscp)来直接测量这样的短代码。

于 2015-03-22T01:51:05.117 回答
1

你试过clock_gettime(CLOCK_MONOTONIC, &tp)吗?应该非常接近手动读取循环计数器,还要记住循环计数器可能在 cpu 内核之间不同步。

于 2015-03-22T01:51:29.120 回答