【问题标题】:CPU Cycle count based profiling in C/C++ Linux x86_64C/C++ Linux x86_64 中基于 CPU 周期计数的分析
【发布时间】:2011-04-19 08:31:51
【问题描述】:

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

【问题讨论】:

  • 显然,函数本身需要一些时钟周期(例如,您在那里进行了位移操作)。为什么不从结果中减去 33(如果你不能使用分析器)假设这个数字是可重现的(它可能不是,例如,如果你有缓存未命中等)?
  • rdtsc 本身是一条相对较慢的指令,它测量“参考”周期,而不管 CPU 当前的核心时钟频率如何。有关详细信息,请参阅How to get the CPU cycle count in x86_64 from C++?,对于内在函数,您可以使用它来代替内联 asm。

标签: c profiling x86-64 cpu rdtsc


【解决方案1】:

我个人认为 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% 的时间内完成。

【讨论】:

    【解决方案2】:

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

    正确的方法是:

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

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

    【讨论】:

    • 更不用说RDTSC之前的序列化会对你的测量产生负面影响。
    • 是的,我知道 CPUID,但我不知道它的作用。关于个人执行,是的,我同意。我正在测试 1000 次运行,并消除了运行 1000 次的时间,我的猜测是数字 33 是​​由于 RDTSC 而出现的。 clock() 并没有真正为我工作。但我会在完整的软件完成后查找 kcachegrind。
    【解决方案3】:

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

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

    1个人发现rdtscgettimeofday() 之类的系统调用更准确,可以进行细粒度测量。

    【讨论】:

    • 谢谢。我需要编写一个最多花费 1 微秒的函数,因此需要使用 rdtsc。除了两次通话之间的“33”之外,到目前为止,我对rdtsc 非常满意。我检查了,cpu 有contant_tsc 标志。
    【解决方案4】:

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

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

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

    【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

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

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

      如果是这样,那么您就在良好的基础上。您可以使用ZoomLTProf 之类的工具。这里是my favorite method.

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 2012-12-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多