使用 RDTSC 在 C 中计算 CPU 频率始终返回 0
-
26-09-2019 - |
题
我们的讲师向我们提供了以下代码,以便我们可以测量一些算法的性能:
#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。这排除了虚拟机上的问题,我一开始还以为是虚拟机的问题。
任何想法为什么会发生这种情况以及如何解决它?
解决方案
好了,因为对方的回答是没有帮助的,我会试着更详细的解释。问题是,现代的CPU可以乱序执行指令。您的代码开始为这样的:
rdtsc
push 1
call sleep
rdtsc
现代的CPU做的不的不一定是原来的顺序,虽然执行指令。尽管原来的顺序,CPU是(主要)免费来执行,就像:
rdtsc
rdtsc
push 1
call sleep
在这种情况下,很显然为什么两个rdtsc
s之间的区别是(至少是非常接近)0。为了防止这种情况,你需要执行的指令,使CPU能的从不的重排执行的顺序进行。以使用为最常用的指令是CPUID
。我联系其他答案应该(如果没有记错)大致从那里开始,大约要正确有效地完成这个任务使用CPUID
/步骤。
当然,它可能是蒂姆帖子是正确的,而你的也的看到,因为虚拟机的问题。尽管如此,因为它代表现在,有没有保证您的代码将正常工作,即使是在实际硬件。
编辑:至于为什么代码的将的工作:嗯,首先,事实说明的可以的是顺序执行了并不能保证他们< EM>将定。其次,它可能是(至少部分实现)sleep
包含序列化防止rdtsc
从它周围被重排指令,而有的则没有(或可能包含的内容,只在特定的(但不确定)的情况下执行它们)。
什么留给你的是,可能只是一个运行和未来之间的几乎任何重新编译改变,甚至行为。它可以产生非常精确的结果数十次成一排,然后失败对于一些(几乎)完全无法解释的原因(例如,一些在某些其他过程完全发生)。
其他提示
我不能肯定地说,究竟是你的代码错误,但你做了相当多的不必要的工作,对于这样一个简单的指令。我建议你充分简化您的rdtsc
代码。你并不需要做的64位数学携带你的自我,你不需要存储操作的双重结果。你不需要在你的内联汇编使用单独的输出,你可以告诉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,还是其他什么东西。
你在你的汇编语句忘了使用volatile
,所以你告诉了asm
语句生成每一次相同的输出,如纯函数的编译器。 (volatile
是只对于没有输出asm
语句隐式的。)
这也解释了为什么你得到的究竟的零:编译器优化end-start
到0
在编译的时候,通过CSE(共子表达式消除)
请参阅我在获得CPU周期数的答案吗?,在__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 << 30)* 4 + LO;
我怀疑,如果你可以安全地在“签名”进行如此巨大的乘法...是不是经常一个32位的数字? ...只是事实,你不能安全地乘以2 ^ 32,不得不将其追加为在年底加入到2 ^ 30一个额外的“* 4”已经在这个可能性的暗示......你可能需要将每个子组件hi和lo为双(而不是在最后一个单一个),并使用两个双打做乘法