使用循环计数器测量程序执行时间



我在这一行中感到困惑-->

result = (double) hi * (1 << 30) * 4 + lo;

以下代码的:

void access_counter(unsigned *hi, unsigned *lo)
// Set *hi and *lo to the high and low order bits of the cycle
// counter. 
{
  asm("rdtscp; movl %%edx,%0; movl %%eax,%1"  // Read cycle counter
      : "=r" (*hi), "=r" (*lo)                // and move results to
      : /* No input */                        // the two outputs
      : "%edx", "%eax");
}
double get_counter()
// Return the number of cycles since the last call to start_counter.
{
    unsigned ncyc_hi, ncyc_lo;
    unsigned hi, lo, borrow;
    double result;
    /* Get cycle counter */
    access_counter(&ncyc_hi, &ncyc_lo);
    lo = ncyc_lo - cyc_lo;
    borrow = lo > ncyc_lo;
    hi = ncyc_hi - cyc_hi - borrow;
    result = (double) hi * (1 << 30) * 4 + lo;
    if (result < 0) {
    fprintf(stderr, "Error: counter returns neg value: %.0fn", result);
    }
    return result;
}

我不能理解的是,为什么hi要乘以2^30,然后再乘以4?然后再加上low?请有人解释一下这行代码中发生了什么。我知道高低包含什么。

简短的答案:

该行将存储为2个32位值的64位整数转换为浮点数。

为什么代码不使用64位整数?好吧,gcc支持64位数字已经有很长一段时间了,但据推测,这段代码早于64位数字。在这种情况下,支持这么大的数字的唯一方法是将它们放入浮点数中。

答案很长:

首先,您需要了解rdtscp是如何工作的。当这个汇编指令被调用时,它做两件事:

1) 将ecx设置为IA32_TSC_AUX MSR。根据我的经验,这通常意味着ecx被设置为零。2) 将edx:eax设置为处理器时间戳计数器的当前值。这意味着计数器的低64位进入eax,而高32位进入edx。

考虑到这一点,让我们看看代码。当从get_counter调用时,access_counter将把edx放在'ncyc_hi'中,把eax放在'sncyc_lo'中然后get_counter将执行以下操作:

lo = ncyc_lo - cyc_lo;
borrow = lo > ncyc_lo;
hi = ncyc_hi - cyc_hi - borrow;

这是干什么的?

由于时间存储在两个不同的32位数字中,如果我们想知道经过了多少时间,我们需要做一些工作来找出旧时间和新时间之间的差异。完成后,结果存储在hi/lo中(再次使用2个32位数字)。

这终于引出了你的问题。

result = (double) hi * (1 << 30) * 4 + lo;

如果我们可以使用64位整数,那么将2个32位值转换为单个64位值将如下所示:

unsigned long long result = hi; // put hi into the 64bit number.
result <<= 32;                  // shift the 32 bits to the upper part of the number
results |= low;                 // add in the lower 32bits.

如果你不习惯比特转换,也许这样看会有所帮助。如果lo=1且high=2,则表示为十六进制数:

result = hi;   0x0000000000000002
result <<= 32; 0x0000000200000000
result |= low; 0x0000000200000001

但是,如果我们假设编译器不支持64位整数,那就行不通了。虽然浮点数可以保持那么大的值,但它们不支持移位。因此,我们需要找到一种方法,将"hi"左移32位,而不使用左移

那么,左移1就等于乘2。左移2等于乘4。左移[省略…]左移32等于乘4294967296。

巧合的是,4294967296==(1<<30)*4。

那么,为什么要用那种复杂的方式来写呢?4294967296是一个相当大的数字。事实上,它太大了,无法放入32位整数中。这意味着,如果我们把它放在源代码中,一个不支持64位整数的编译器可能很难弄清楚如何处理它。这样写,编译器可以生成处理这个大数字所需的任何浮点指令。

当前代码错误的原因:

这个代码的变体似乎已经在互联网上流传了很长一段时间。最初(我认为)access_counter是使用rdtsc而不是rdtscp编写的。除了指出rdtsc不设置ecx,rdtscp设置ecx之外,我不会试图描述两者之间的区别(用谷歌搜索它们)。不管是谁把rdtsc改成了rdtscp,显然都不知道这一点,也没有调整内联汇编程序来反映它。尽管如此,你的代码可能工作得很好,但它可能会做一些奇怪的事情。要修复它,你可以做:

asm("rdtscp; movl %%edx,%0; movl %%eax,%1"  // Read cycle counter
  : "=r" (*hi), "=r" (*lo)                  // and move results to
  : /* No input */                          // the two outputs
  : "%edx", "%eax", "%ecx");

虽然这是可行的,但它不是最佳的。寄存器是i386上一种宝贵而稀缺的资源。这个微小的碎片使用了其中的5个。稍作修改:

asm("rdtscp"  // Read cycle counter
  : "=d" (*hi), "=a" (*lo)
  : /* No input */
  : "%ecx");

现在我们少了2个汇编语句,并且只使用了3个寄存器。

但即使这样也不是我们能做的最好的。在编写这段代码以来的(可能很长的)时间里,gcc添加了对64位整数的支持和读取tsc的函数,所以你根本不需要使用asm:

unsigned int a;
unsigned long long result;
result =  __builtin_ia32_rdtscp(&a);

"a"是在ecx中返回的(无用的?)值。函数调用需要它,但我们可以忽略返回的值。

因此,与其做这样的事情(我认为您现有的代码会这样做):

unsigned cyc_hi, cyc_lo;
access_counter(&cyc_hi, &cyc_lo);
// do something
double elapsed_time = get_counter(); // Find the difference between cyc_hi, cyc_lo and the current time

我们可以做到:

unsigned int a;
unsigned long long before, after;
before =  __builtin_ia32_rdtscp(&a);
// do something
after =  __builtin_ia32_rdtscp(&a);
unsigned long long elapsed_time = after - before;

它更短,不使用难以理解的汇编程序,更容易阅读、维护并生成尽可能好的代码。

但它确实需要相对较新版本的gcc。

最新更新