用RDTSC在C语言中计算CPU频率总是返回0

4
我们的导师给了我们下面这段代码,以便我们可以测量一些算法的性能:
#include <stdio.h>
#include <unistd.h>

static unsigned cyc_hi = 0, cyc_lo = 0;

static void access_counter(unsigned *hi, unsigned *lo) {
    asm("rdtsc; movl %%edx,%0; movl %%eax,%1"
    : "=r" (*hi), "=r" (*lo)
    : /* No input */
    : "%edx", "%eax");
}

void start_counter() {
    access_counter(&cyc_hi, &cyc_lo);
}

double get_counter() {
    unsigned ncyc_hi, ncyc_lo, hi, lo, borrow;
    double result;

    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;

    return result;
}

然而,我需要这段代码能够在不同CPU频率的机器上移植运行。为此,我正在尝试像这样计算运行代码的机器的CPU频率:

int main(void)
{
    double c1, c2;

    start_counter();

    c1 = get_counter();
    sleep(1);
    c2 = get_counter();

    printf("CPU Frequency: %.1f MHz\n", (c2-c1)/1E6);
    printf("CPU Frequency: %.1f GHz\n", (c2-c1)/1E9);

    return 0;
}

问题在于结果总是为0,我不明白为什么。我作为虚拟机的客户端在VMware上运行Linux(Arch)。

在朋友的电脑上(MacBook),它在某种程度上工作;我的意思是,结果大于0,但它是可变的,因为CPU频率不固定(我们尝试修复它,但由于某些原因我们无法做到)。他有一台不同的机器,主机运行Linux(Ubuntu),也报告为0。这排除了问题出现在虚拟机上的可能性,这是我最初认为的问题所在。

你们有什么想法,为什么会发生这种情况,我该如何解决?


2
虽然问题有点不同,但我在http://stackoverflow.com/questions/2658699/measure-time-to-execute-single-instruction/2658833#2658833的大部分回答也适用于这里。 - Jerry Coffin
@Jerry Coffin,我不知道你在那个问题中的回答是如何帮助我的。但是话说回来,我并没有理解你写的大部分内容,哈哈。 - rfgamaral
@Tim Post:他的问题不是(至少不完全是)VMWare - 而是RDTSC可以被乱序执行,因此如果没有执行序列化指令(通常为CPUID),它会产生几乎毫无意义的结果。 - Jerry Coffin
1
编写一个小循环,重复打印 get_counter() 的输出。确保它正在计数。 - ajs410
2
如果你想测量墙上时钟时间,不要测量周期并尝试转换为时间;直接测量时间(例如使用gettimeofday()clock_gettime(),其中包括CLOCK_MONOTONICCLOCK_PROCESS_CPUTIME_ID)。 - caf
显示剩余7条评论
5个回答

2
我无法确定你的代码出了什么问题,但是你为一个简单的指令做了很多不必要的工作。我建议你大大简化rdtsc代码。你不需要自己进行64位数学进位,也不需要将该操作的结果存储为double类型。你不需要在内联汇编中使用单独的输出,可以告诉GCC使用eax和edx。

这是一个大大简化后的代码:

#include <stdint.h>

uint64_t rdtsc() {
    uint64_t ret;

# if __WORDSIZE == 64
    asm ("rdtsc; shl $32, %%rdx; or %%rdx, %%rax;"
        : "=A"(ret)
        : /* no input */
        : "%edx"
    );
#else
    asm ("rdtsc" 
        : "=A"(ret)
    );
#endif
    return ret;
}

此外,您应考虑打印出从中获取的值,以查看是否获取到了0或其他值。


我在编译您的代码时遇到了错误:expected string literal before ')' token - rfgamaral
现在它已经编译成功了,谢谢你的示例。然而,我仍然得到一个零的频率。我不认为问题在于rdtsc的实现方式。既然这不能解决我的问题,我宁愿使用教师提供的代码。 - rfgamaral
必须使用volatile,否则编译器可以将两次具有相同输入的内联汇编语句进行CSE,并假定它将获得相同的结果。(在这种情况下,输入不存在)。这就是为什么@RicardoAmaral可能会得到零的原因。此外,没有理由在asm内部执行移位/或操作,只需使用“=a”和“=d”输出即可。或者更好的方法是使用<immintrin.h>中的__rdtsc(),尽管这可能在2010年不存在。请参见Get CPU cycle count?,mysticial在同一问题上的答案具有可行的asm。 - Peter Cordes

2

好的,由于另一个答案没有帮助,我会尝试更详细地解释。问题在于现代CPU可以乱序执行指令。你的代码开始时像这样:

rdtsc
push 1
call sleep
rdtsc

现代CPU并不一定按照它们原来的顺序执行指令。尽管按照原始顺序,但CPU(大多数情况下)可以自由地执行它们,就像这样:

rdtsc
rdtsc
push 1
call sleep

在这种情况下,两个rdtsc之间的差异为什么会(至少非常接近)为0是很清楚的。为了防止这种情况发生,您需要执行一条CPU永远不会重新排列以无序执行的指令。用于此任务的最常见指令是CPUID。我链接的另一个答案应该(如果我没记错的话)从那里开始,介绍正确/有效使用CPUID所需的步骤。
当然,Tim Post可能是对的,您也可能因为虚拟机而遇到问题。尽管如此,目前为止,您的代码即使在真实硬件上也不能保证正常工作。
编辑:至于代码为什么能够正常工作:首先,指令可以被无序执行并不意味着它们一定会被无序执行。其次,sleep的某些实现可能包含序列化指令,可以防止rdtsc在其中被重新排列,而其他实现则不然(或者可能包含它们,但仅在特定(但未指定)情况下执行它们)。
您得到的是一种行为,几乎可以在任何重新编译甚至仅在一次运行和下一次之间发生变化。它可能连续几十次产生极其准确的结果,然后因某些(几乎)完全无法解释的原因而失败(例如,在完全不同的进程中发生的事情)。

我刚刚深入研究了VMWare的时间保持规范,他的代码“应该”可以工作。从文档中可以看出,rdtsc“应该”按预期工作,尽管我相当确定在进行虚拟化时精度会更高。 - Tim Post
4
乱序执行并不会解决他的问题。睡眠不是一个单一的指令,任何乱序执行都不能重新排列两个连续的 RDTSC 指令。即使这样做了,在英特尔 CPU 上,每个时钟周期 rdtsc 都会增加一次。即使连续调用两次,也不可能返回相同的值。 - SoapBox
我同意SoapBox的观点 - 毕竟,在sleep()函数内部是对内核的调用;在这个call后面隐藏着非常多的指令。 - caf
1
@Nazgulled:如果你真的得到了0的差异(不仅仅是一个看起来比较小的数字),那么我必须说,这听起来像是虚拟机引起的问题,无论他们的文档怎么说。在一个(正常工作的)单核上,没有两个RDTSC的执行应该返回相同的值(除非在它们之间使用WRMSR进行修改)。 - Jerry Coffin
我的猜测是问题出在sleep()调用上,而不是RDTSC。我这么说是因为只有在sleep()时差异才为0。如果我实现一个需要1秒钟执行的循环,那么时间差异将超过0。 - rfgamaral
显示剩余2条评论

1
你在汇编语句中忘记使用volatile,这告诉编译器asm语句每次产生相同的输出,就像一个纯函数。(对于没有输出的asm语句,volatile是隐含的。)

这解释了为什么你得到完全为零:编译器通过CSE(公共子表达式消除)在编译时将end-start优化为0

请参见我的答案Get CPU cycle count?中的__rdtsc()内部函数,@Mysticial的答案中有工作的GNU C内联汇编代码,我在此引用:

// prefer using the __rdtsc() intrinsic instead of inline asm at all.
uint64_t rdtsc(){
    unsigned int lo,hi;
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}
这对于32位和64位代码的工作是正确且高效的。

1
关于VMWare,请查看时间保持规范(PDF链接)以及此线程。 TSC指令是(取决于客户操作系统):
  • 直接传递给真实硬件(PV guest)
  • 在VM在主机处理器上执行时计算周期数while(Windows /等)

请注意,在#2中,while VM正在主机处理器上执行。如果我记得正确,Xen也会出现同样的现象。本质上,您可以期望代码应该在半虚拟化的guest上按预期工作。如果进行模拟,则完全不可能期望类似硬件的一致性。


0

嗯,我不确定,但我怀疑问题可能在这行代码里:

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

我怀疑您能否安全地在“unsigned”中进行如此大的乘法运算...那通常是32位数字吧? ...仅仅是您无法安全地乘以2^32并且必须将其附加为额外的“* 4”,并已在末尾添加了2^30,这已经暗示了这种可能性...您可能需要将每个子组件hi和lo转换为double(而不是在最后一个单一组件)并使用两个双倍数进行乘法运算。


求值顺序是从左到右。它实际上是 (((double)hi) * (1<<30)) * 4.0 + (double)lo。但是,((uint64_t)hi<<32) + lo 是正常的做法。或者通常的技巧是使用 1ULL<<32 来左移一个64位整数。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接