测量我的 C 程序循环次数的最可靠方法是什么?

What is the most reliable way to measure the number of cycles of my program in C?

我熟悉两种方法,但它们都有其局限性。

第一个是使用指令RDTSC。但是,问题是它不单独计算我的程序的周期数,因此对并发进程引起的噪声很敏感。

第二种选择是使用clock库函数。我认为这种方法是可靠的,因为我希望它只计算我的程序的周期数(我打算实现的)。然而,事实证明,在我的例子中,它测量了经过的时间,然后将它乘以 CLOCKS_PER_SEC。这不仅不可靠,而且是错误的,因为 CLOCKS_PER_SEC 设置为 1,000,000,这与我处理器的实际频率不符。

鉴于所提出方法的局限性,是否有更好、更可靠的替代方法来产生一致的结果?

RDTSC 是计算程序执行周期的最准确方法。如果您希望在线程被抢占的情况下随时间尺度测量执行性能,那么使用分析器(例如 VTune)可能会更好。

与几乎没有开销的 RDTSC 相比,

CLOCKS_PER_SECOND/clock() 几乎是一种非常糟糕的(低性能)获取时间的方式。

如果您有 RDTSC 的具体问题,我可以提供帮助。


回复:评论

Intel Performance Counter Monitor:这个主要是用来测量处理器以外的指标,比如Memory bandwidth, power usage, PCIe utilization。它也恰好测量 CPU 频率,但它通常对处理器绑定的应用程序性能没有用。

RDTSC 可移植性:RDTSC 是所有现代英特尔 CPU 支持的英特尔 CPU 指令。在现代 CPU 上,它基于 CPU 的非核心频率并且在 CPU 核心之间有些相似,但如果您的应用程序经常被抢占到不同的核心(尤其是不同的插座)。如果是这样的话,你真的想看看分析器。

乱序执行: 是的,事情会乱序执行,所以这会稍微影响性能,但执行指令仍然需要时间,RDTSC 是最好的方式测量那个时间。它在同一内核上执行非 IO 绑定指令的正常用例中表现出色,而这正是它的用途所在。如果您有更复杂的用例,您确实应该使用不同的工具,但这并不能否定 rdtsc() 在分析程序执行方面非常有用。

这里很大程度上取决于您尝试测量的时间量。

如果正确使用,RDTSC 可以(几乎)100% 可靠。然而,它主要用于测量真正微观的代码片段。如果你想测量两个序列,比如说,每个序列大约几十条指令,可能没有其他方法可以完成这项工作。

虽然正确使用它有点挑战。一般来说,要获得良好的测量结果,您至少需要执行以下操作:

  1. 将代码设置为仅在一个特定核心上 运行。
  2. 将代码设置为以最高优先级执行,这样就没有任何东西可以抢占它。
  3. 自由使用 CPUID 以确保在需要时进行序列化。

另一方面,如果您要测量的时间从 100 毫秒开始,那么 RDTSC 毫无意义。这就像试图用千分尺测量城市之间的距离。为此,通常最好确保所讨论的代码(至少)花费一秒钟左右的时间。 clock 不是特别精确,但在这个一般顺序的一段时间内,它可能只精确到 10 毫秒左右这一事实或多或少是无关紧要的。

Linux perf_event_open 系统调用 config = PERF_COUNT_HW_CPU_CYCLES

这个系统调用有明确的控制:

  • 处理 PID 选择
  • 是否考虑kernel/hypervisor说明

因此,即使多个进程同时 运行,它也会正确计算周期数。

有关详细信息,请参阅此答案:How to get the CPU cycle count in x86_64 from C++?

perf_event_open.c

#include <asm/unistd.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <inttypes.h>

static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                    group_fd, flags);
    return ret;
}

int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count;
    int fd;

    uint64_t n;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 10000;
    }

    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_CPU_CYCLES;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    // Don't count hypervisor events.
    pe.exclude_hv = 1;

    fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
        fprintf(stderr, "Error opening leader %llx\n", pe.config);
        exit(EXIT_FAILURE);
    }

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    /* Loop n times, should be good enough for -O0. */
    __asm__ (
        "1:;\n"
        "sub , %[n];\n"
        "jne 1b;\n"
        : [n] "+r" (n)
        :
        :
    );

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));

    printf("%lld\n", count);

    close(fd);
}