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

时间:2010-09-30 13:19:41

标签: c profiling cpu cpu-usage 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”的差异。 有什么建议吗?

7 个答案:

答案 0 :(得分:5)

我个人认为rdtsc指令很棒,可用于各种任务。我不认为使用cpuid是准备rdtsc所必需的。以下是我对rdtsc的理由:

  1. 由于我使用Watcom编译器,我使用“#pragma aux”实现了rdtsc,这意味着C编译器将生成内联指令,期望edx:eax中的结果,并通知其优化器eax的内容和edx已被修改。与传统的_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等待,我使用毫秒=&gt; (DIFF + 1500000)/ 3000000。这些是我的“标准”单位。我很少用秒。
  5. 有时候我会得到意想不到的缓慢结果然后我必须问自己:这是由于中断还是代码?我测量了几次,看看它是否确实是一个中断。在那种情况下......好的中断在现实世界中一直发生。如果我的序列很短,那么下一次测量很可能不会被中断。如果序列较长,则会更频繁地发生中断,并且我无能为力。
  6. 非常准确地测量经过的时间(我们或更低的时间和更长的ET)将增加在divide_U8_by_U4中获得除法异常的风险,因此我想到何时使用我们以及何时使用ms。
  7. 我还有基本统计代码。使用这个我记录最小值和最大值,我可以计算平均值和标准偏差。这段代码非常重要,因此必须从测量的ET中减去自己的ET。
  8. 如果编译器正在进行大量优化并且您的读数存储在局部变量中,则编译器可以确定(“正确地”)可以省略代码。避免这种情况的一种方法是将结果存储在公共(非静态,非堆栈)变量中。
  9. 在现实条件下运行的程序应该在真实条件下进行测量,没有办法解决这个问题。
  10. 关于时间戳计数器准确的问题,我会说假设不同核心上的tsc是同步的(这是常态),在低活动期间存在CPU节流的问题以减少能量消耗。测试时始终可以禁用功能。如果您在同一处理器上执行1 GHz或10 Mhz的指令,则经过的周期计数将是相同的,即使前者在1%的时间内完成并且与后者相符。

答案 1 :(得分:2)

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

正确的方法是:

  • 计算对函数进行大量调用所需的周期数或CPU时间(使用clock()),然后对它们取平均值;或
  • 使用循环级仿真分析器,如Callgrind / kcachegrind

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:1)

TSC不是一个很好的时间衡量标准。 CPU对TSC的唯一保证是它单调上升(也就是说,如果你RDTSC一次然后再次执行,第二个将返回一个高于第一个的结果)并且它将用很长时间来解决。

答案 5 :(得分:0)

我是否理解为什么你这样做的原因是用它来包含其他代码,以便你可以测量其他代码需要多长时间?

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

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

如果是这样的话,那你就处于良好的状态。您可以使用ZoomLTProf等工具。这是my favorite method.

答案 6 :(得分:0)

使用perf_event_open的Linux 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);
}