9

我正在使用以下代码来分析我的操作,以优化我的函数中采用的 cpu 周期。

static __inline__ unsigned long GetCC(void)
{
  unsigned a, d; 
  asm volatile("rdtsc" : "=a" (a), "=d" (d)); 
  return ((unsigned long)a) | (((unsigned long)d) << 32); 
}

我认为这不是最好的,因为即使连续两次通话也给我带来了“33”的差异。有什么建议么 ?

4

7 回答 7

7

我个人认为 rdtsc 指令非常棒,可用于各种任务。我认为没有必要使用 cpuid 来准备 rdtsc。以下是我对 rdtsc 的推理:

  1. 由于我使用 Watcom 编译器,我使用“#pragma aux”实现了 rdtsc,这意味着 C 编译器将生成内联指令,期望结果在 edx:eax 中,并通知其优化器 eax 和 edx 的内容已经修改的。这是对传统 _asm 实现的巨大改进,在传统 _asm 实现中优化器将远离在 _asm 附近进行优化。我还使用“#pragma aux”实现了 divide_U8_by_U4,这样当我将 clock_cycles 转换为 us 或 ms 时就不需要调用 lib 函数。
  2. rdtsc 的每次执行都会导致一些开销(如果按照作者的示例进行封装,则会更多),必须更多地考虑要测量的序列越短。一般来说,我不会为比内部时钟频率的 1/30 更短的序列计时,这通常可以达到 1/10^8 秒(3 GHZ 内部时钟)。我使用这些测量作为指示,而不是事实。知道这一点,我可以省略 cpuid。我测量的次数越多,我就越接近事实。
  3. 为了可靠地测量,我会使用 1/100 - 1/300 范围,即 0.03 - 0.1 us。在这个范围内,使用 cpuid 的额外精度实际上是微不足道的。我将此范围用于短序列计时。这是我的“非标准”单元,因为它取决于 CPU 的内部时钟频率。例如,在 1 GHz 机器上,我不会使用 0.03 us,因为这会使我超出 1/100 的限制,我的读数将成为指示。这里我将使用 0.1 us 作为最短时间测量单位。不会使用 1/300,因为它太接近 1 us(见下文)而不会产生任何显着差异。
  4. 对于更长的处理序列,我将两个 rdtsc 读数之间的差异除以 3000(对于 3 GHz),并将经过的时钟周期转换为我们。实际上我使用 (diff+1500)/3000,其中 1500 是 3000 的一半。对于 I/O 等待,我使用毫秒 => (diff+1500000)/3000000。这些是我的“标准”单位。我很少用秒。
  5. 有时我得到出乎意料的缓慢结果,然后我必须问自己:这是由于中断还是代码?我又测量了几次,看看它是否确实是一个中断。在那种情况下……在现实世界中,井中断一直在发生。如果我的序列很短,那么下一次测量很可能不会被中断。如果序列更长,中断会更频繁地发生,我对此无能为力。
  6. 非常准确地测量很长的经过时间(小时和更长的 ET 在我们或更低)将增加在 divide_U8_by_U4 中出现除法异常的风险,所以我考虑何时使用我们以及何时使用 ms。
  7. 我也有基本统计的代码。使用这个我记录最小值和最大值,我可以计算平均值和标准偏差。此代码非常重要,因此必须从测量的 ET 中减去它自己的 ET。
  8. 如果编译器正在进行广泛的优化并且您的读数存储在局部变量中,编译器可能会确定(“正确”)代码可以省略。避免这种情况的一种方法是将结果存储在公共(非静态、非基于堆栈的)变量中。
  9. 在现实世界条件下运行的程序应该在现实世界条件下进行测量,这是没有办法的。

至于时间戳计数器是否准确的问题,我想说假设不同内核上的 tsc 是同步的(这是常态),在低活动期间会出现 CPU 节流问题以降低能耗。测试时总是可以禁止该功能。如果您在同一处理器上以 1 GHz 或 10 Mhz 执行指令,则经过的周期计数将是相同的,即使前者在 1% 的时间内完成,也就是后者。

于 2010-12-07T10:25:33.160 回答
2

尝试计算单个函数执行的周期并不是真正正确的方法。您的进程可以随时中断,以及缓存未命中和分支错误预测导致的延迟,这意味着从调用到调用的周期数可能存在相当大的偏差。

正确的方法是:

  • clock()计算大量调用函数所花费的周期数或 CPU 时间(用),然后将它们平均;或者
  • 使用像Callgrind / kcachegrind这样的循环级模拟分析器。

顺便说一句,您需要在RDTSC. 通常CPUID使用。

于 2010-09-30T13:48:47.990 回答
2

你在正确的轨道上1,但你需要做两件事:

  1. 之前运行cpuid指令rdtsc以刷新 CPU 流水线(使测量更可靠)。据我记得它clobbers 注册从eaxedx
  2. 实时测量。执行时间还有很多,而不仅仅是 CPU 周期(锁定争用、上下文切换和其他您无法控制的开销)。实时校准 TSC 刻度。您可以在一个简单的循环中执行此操作,该循环需要测量gettimeofday(Linux,因为您没有提到平台)调用和rdtsc输出的差异。然后你可以知道每个 TSC 滴答需要多少时间。另一个考虑因素是跨 CPU 的 TSC 同步,因为每个内核可能有自己的计数器。在 Linux 中你可以看到它/proc/cpuinfo,你的 CPU 应该有一个constant_tsc标志。我见过的大多数较新的 Intel CPU 都有这个标志。

1个人发现它比细粒度测量rdtsc等系统调用更准确。gettimeofday()

于 2010-09-30T14:03:11.737 回答
2

您可能需要担心的另一件事是,如果您在多核机器上运行,程序可能会移动到不同的核心,该核心将具有不同的 rdtsc 计数器。不过,您可以通过系统调用将进程固定到一个核心。

如果我试图测量这样的东西,我可能会将时间戳记录到一个数组中,然后在完成基准测试的代码后返回并检查这个数组。当您检查记录到时间戳数组中的数据时,您应该记住该数组将依赖于 CPU 缓存(如果您的数组很大,可能会分页),但是您可以预取或在分析时记住这一点数据。您应该会在时间戳之间看到一个非常规则的时间增量,但有几个尖峰和可能的几个下降(可能是由于移动到不同的核心)。常规时间增量可能是您最好的测量,因为它表明没有外部事件影响这些测量。

话虽如此,如果您要进行基准测试的代码具有不规则的内存访问模式或运行时间,或者依赖于系统调用(尤其是与 IO 相关的调用),那么您将很难将噪声与您感兴趣的数据区分开来。

于 2010-09-30T14:53:36.687 回答
1

TSC 不是衡量时间的好方法。CPU 对 TSC 做出的唯一保证是它单调上升(也就是说,如果你RDTSC一次然后再做一次,第二个将返回高于第一个的结果)并且它会花费很长时间很长的时间来环绕。

于 2010-09-30T13:54:30.340 回答
0

我是否正确理解您这样做的原因是用它括住其他代码,以便您可以测量其他代码需要多长时间?

我相信您知道另一种好方法就是将其他代码循环 10 ^ 6 次,秒表,然后将其称为微秒。

一旦您测量了其他代码,我是否正确假设您想知道其中哪些行值得优化,以减少花费的时间?

如果是这样,那么你就在良好的基础上。您可以使用ZoomLTProf 之类的工具。这是我最喜欢的方法。

于 2010-09-30T16:52:24.977 回答
0

Linuxperf_event_open系统调用与config = PERF_COUNT_HW_CPU_CYCLES

这个 Linux 系统调用似乎是性能事件的跨体系结构包装器。

这个答案与这个 C++ 问题的答案基本相同: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 $1, %[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);
}
于 2020-11-18T17:23:29.717 回答