rdtsc测量函数的时间

时间:2015-03-22 01:20:17

标签: performance gcc x86-64 rdtsc

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

  1. 循环调用它。聚合循环中的每个rdtsc差异并除以调用次数。 (假设这是N)
  2. 循环调用它。获取循环本身的rdtsc差异并除以N。
  3. 但我看到一些不一致的行为。

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

    3. 代码如下。

          #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成正比,与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的值更小或略小,因为它不包含循环控制开销。
      3. 问题

        总而言之,我的问题是

        1. 为什么增加N时两种方法给出的值都会发生如此大的变化?特别是方法1,它不考虑循环控制开销。

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

        3. 修改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
          

          有什么收获?我想把它们当作内联方法,因为我看到它在多个地方使用它。

          一个。评论中的解决方案。

2 个答案:

答案 0 :(得分:2)

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

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

  

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

英特尔最近使用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节中提出的问题的解决方案是添加CPUID指令   就在RDTPSCP和两个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读取时间戳寄存器,并将值存储在中   记忆。   然后执行我们要测量的代码。如果代码是对a的调用   函数,建议声明这样的函数为“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个uop(不可融合)和24个滴答的延迟; RDTSCP(对于较旧的微体系结构,Sandy Bridge有23个uops和36个滴答延迟,而21个uops和28个滴答用于rdtsc)。所以,你不能使用普通的rdtsc(或rdtscp)直接测量这样的短代码。

答案 1 :(得分:1)

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