我们的讲师向我们提供了以下代码,以便我们可以测量一些算法的性能:

#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

在这种情况下,很显然为什么两个rdtscs之间的区别是(至少是非常接近)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,还是其他什么东西。

至于VMWare,看看 计时规范 (PDF 链接),以及 这个线程. 。TSC 指令为(取决于来宾操作系统):

  • 直接传递到真实硬件(PV guest)
  • 计数周期 尽管 VM 在主机处理器(Windows / 等)上执行

请注意,在 #2 中 尽管 VM 在主机处理器上执行。如果我没记错的话,Xen 也会出现同样的现象。本质上,您可以预期代码应该在半虚拟化来宾上按预期工作。如果进行模拟,那么期望像硬件这样的一致性是完全不合理的。

你在你的汇编语句忘了使用volatile ,所以你告诉了asm语句生成每一次相同的输出,如纯函数的编译器。 (volatile是只对于没有输出asm语句隐式的。)

这也解释了为什么你得到的究竟的零:编译器优化end-start0在编译的时候,通过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为双(而不是在最后一个单一个),并使用两个双打做乘法

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top