繁体   English   中英

C/C++ Linux x86_64 中基于 CPU 周期计数的分析

[英]CPU Cycle count based profiling in C/C++ Linux x86_64

我正在使用以下代码来分析我的操作,以优化我的函数中采用的 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”的差异。 有什么建议吗?

我个人认为 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 范围 i/e 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 中出现除法异常的风险,因此我考虑何时使用我们以及何时使用毫秒。
  7. 我也有基本统计的代码。 使用这个我记录最小值和最大值,我可以计算平均值和标准偏差。 此代码非常重要,因此必须从测量的 ET 中减去它自己的 ET。
  8. 如果编译器正在进行大量优化并且您的读数存储在局部变量中,则编译器可能会(“正确地”)确定可以省略代码。 避免这种情况的一种方法是将结果存储在公共(非静态、非基于堆栈的)变量中。
  9. 在现实世界条件下运行的程序应该在现实世界条件下进行衡量,这是没有办法的。

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

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

正确的方法是:

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

顺便说一句,您需要在RDTSC之前执行序列化指令。 通常使用CPUID

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

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

1个人发现rdtscgettimeofday()等系统调用更准确,用于细粒度测量。

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

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

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

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

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

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

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

如果是这样,那么您就处于良好状态。 您可以使用ZoomLTProf 之类的工具。 这是我最喜欢的方法。

Linux perf_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);
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM