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。这排除了虚拟机上的问题,我最初认为这是问题所在。

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

4

5 回答 5

2

好的,由于其他答案没有帮助,我将尝试更详细地解释。问题是现代 CPU 可以乱序执行指令。你的代码开始是这样的:

rdtsc
push 1
call sleep
rdtsc

但是,现代 CPU不一定按原始顺序执行指令。尽管您有原始订单,但 CPU (大部分)可以自由执行,就像:

rdtsc
rdtsc
push 1
call sleep

在这种情况下,很清楚为什么两个rdtscs 之间的差异会(至少非常接近)0。为了防止这种情况,您需要执行一条 CPU永远不会重新排列以乱序执行的指令。最常用的指令是CPUID. 我链接的另一个答案应该(如果有记忆的话)大致从那里开始,关于CPUID正确/有效地使用此任务所需的步骤。

当然,Tim Post 可能是对的,而且您看到由于虚拟机而出现的问题。尽管如此,就目前而言,不能保证您的代码即使在真实硬件上也能正常工作。

编辑:至于代码为什么起作用:嗯,首先,指令可以乱序执行的事实并不能保证它们如此。其次,有可能(至少一些实现)sleep包含序列化指令,防止rdtsc围绕它重新排列,而其他不(或可能包含它们,但仅在特定(但未指定)情况下执行它们)。

剩下的就是几乎任何重新编译都会改变的行为,甚至只是在一次运行和下一次运行之间。它可以连续数十次产生极其准确的结果,然后由于某些(几乎)完全无法解释的原因而失败(例如,完全在某些其他过程中发生的事情)。

于 2010-05-11T21:47:05.277 回答
2

我不能确定你的代码到底有什么问题,但你为这么简单的指令做了很多不必要的工作。我建议您rdtsc大幅简化代码。您不需要进行 64 位数学运算,也不需要将该操作的结果存储为双精度数。您不需要在内联 asm 中使用单独的输出,您可以告诉 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 或其他内容。

于 2010-05-11T22:15:32.027 回答
1

至于 VMWare,请查看时间保持规范(PDF 链接)以及此线程。TSC 指令是(取决于客户操作系统):

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

请注意,在 #2 中 VM 正在主机处理器上执行。如果我没记错的话,Xen 也会出现同样的现象。从本质上讲,您可以期望代码在半虚拟化客户机上按预期工作。如果被模拟,期望硬件像一致性是完全不合理的。

于 2010-05-11T22:01:49.983 回答
1

您忘记volatile在 asm 语句中使用,因此您告诉编译器该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 位代码。

于 2018-08-18T13:01:35.823 回答
0

嗯,我不是很肯定,但我怀疑问题可能出在这一行:

结果 = (双) hi * (1 << 30) * 4 + lo;

我怀疑您是否可以安全地在“无符号”中进行如此巨大的乘法运算……那通常不是32位数字吗?...只是您无法安全地乘以 2^32 并且必须将其作为额外的“* 4”附加到最后添加到 2^30 的事实已经暗示了这种可能性...您可能需要将每个子组件 hi 和 lo 转换为双精度(而不是最后的单个)并使用两个双精度进行乘法运算

于 2011-11-09T14:17:24.540 回答