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

时间:2016-04-16 16:55:05

标签: inline-assembly performancecounter execution-time

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

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: %.0f\n", result);
    }
    return result;
}

我无法理解的是,为什么hi乘以2 ^ 30然后4?然后低添加到它?有人请解释这行代码中发生的事情。我确实知道hi和low包含的内容。

1 个答案:

答案 0 :(得分:1)

答案简短:

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

为什么代码不使用64位整数?好吧,gcc长期以来一直支持64位数字,但可能这个代码早于此。在这种情况下,支持大数字的唯一方法是将它们放入浮点数。

答案很长:

首先,您需要了解rdtscp的工作原理。调用此汇编程序指令时,它会执行两项操作:

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

考虑到这一点,让我们看看代码。当从get_counter调用时,access_counter将把edx放入&#39; ncyc_hi&#39;和“ncyc_lo”中的eax。&#39;然后get_counter会这样做:

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

这是做什么的?

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

这最终将我们带到了您的问题。

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位整数,那么它就不会起作用。虽然浮点数可以保持较大的值,但它们不支持移位。因此,我们需要找到一种方法来转移&#39; hi&#39;剩下32位,没有使用左移。

好吧那么,向左移动1实际上与乘以2相同。向左移动2与乘以4相同向左移动[省略...]向左移动32与乘以相同4294967296。

令人惊讶的巧合,4,294,967,296 ==(1 <&lt; 30)* 4.

那么为什么要以复杂的方式写呢?那么,4,294,967,296是一个相当大的数字。事实上,它太大了,不适合32位整数。这意味着如果我们将它放在我们的源代码中,那么不支持64位整数的编译器可能无法确定如何处理它。写得像这样,编译器可以生成可能需要处理的任何浮点指令。

为什么当前代码错误:

看起来这段代码的变体已经在互联网上流传了很长时间。最初(我假设)access_counter是使用rdtsc而不是rdtscp编写的。我不会试图描述两者之间的区别(谷歌他们),除了指出rdtsc没有设置ecx,而rdtscp确实如此。谁将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);

&#39;一个&#39;是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。