【问题标题】:Variance in RDTSC overheadRDTSC 开销的差异
【发布时间】:2011-09-19 22:23:40
【问题描述】:

当我在一些原始图像处理操作中尝试使用 SIMD 指令内在函数时,我正在构建一个微基准来测量性能变化。但是,编写有用的微基准很困难,所以我想首先了解(并尽可能消除)尽可能多的变化和错误来源。

我必须考虑的一个因素是测量代码本身的开销。我正在使用 RDTSC 进行测量,并且正在使用以下代码来查找测量开销:

extern inline unsigned long long __attribute__((always_inline)) rdtsc64() {
    unsigned int hi, lo;
        __asm__ __volatile__(
            "xorl %%eax, %%eax\n\t"
            "cpuid\n\t"
            "rdtsc"
        : "=a"(lo), "=d"(hi)
        : /* no inputs */
        : "rbx", "rcx");
    return ((unsigned long long)hi << 32ull) | (unsigned long long)lo;
}

unsigned int find_rdtsc_overhead() {
    const int trials = 1000000;

    std::vector<unsigned long long> times;
    times.resize(trials, 0.0);

    for (int i = 0; i < trials; ++i) {
        unsigned long long t_begin = rdtsc64();
        unsigned long long t_end = rdtsc64();
        times[i] = (t_end - t_begin);
    }

    // print frequencies of cycle counts
}

运行此代码时,我得到如下输出:

Frequency of occurrence (for 1000000 trials):
234 cycles (counted 28 times)
243 cycles (counted 875703 times)
252 cycles (counted 124194 times)
261 cycles (counted 37 times)
270 cycles (counted 2 times)
693 cycles (counted 1 times)
1611 cycles (counted 1 times)
1665 cycles (counted 1 times)
... (a bunch of larger times each only seen once)

我的问题是:

  1. 以上代码产生的循环计数双峰分布的可能原因是什么?
  2. 为什么最快的时间(234 个周期)只出现少数几次 - 什么极不寻常的情况会减少计数?

更多信息

平台:

  • Linux 2.6.32 (Ubuntu 10.04)
  • g++ 4.4.3
  • 酷睿2双核(E6600);这具有恒定速率 TSC。

SpeedStep 已关闭(处理器设置为性能模式并以 2.4GHz 运行);如果在“按需”模式下运行,我会在 243 和 252 个周期处获得两个峰值,在 360 和 369 个周期处有两个(可能是对应的)峰值。

我正在使用sched_setaffinity 将进程锁定到一个核心。如果我在每个内核上依次运行测试(即锁定到内核 0 并运行,然后锁定到内核 1 并运行),我得到两个内核的相似结果,除了 234 个周期的最快时间往往会稍微出现核心 1 上的次数少于核心 0 上的次数。

编译命令为:

g++ -Wall -mssse3 -mtune=core2 -O3 -o test.bin test.cpp

GCC 为核心循环生成的代码是:

.L105:
#APP
# 27 "test.cpp" 1
    xorl %eax, %eax
    cpuid
    rdtsc
# 0 "" 2
#NO_APP
    movl    %edx, %ebp
    movl    %eax, %edi
#APP
# 27 "test.cpp" 1
    xorl %eax, %eax
    cpuid
    rdtsc
# 0 "" 2
#NO_APP
    salq    $32, %rdx
    salq    $32, %rbp
    mov %eax, %eax
    mov %edi, %edi
    orq %rax, %rdx
    orq %rdi, %rbp
    subq    %rbp, %rdx
    movq    %rdx, (%r8,%rsi)
    addq    $8, %rsi
    cmpq    $8000000, %rsi
    jne .L105

【问题讨论】:

  • 我自己的测量代码得到了完全相同的差异
  • @drhirsch:很高兴知道。你能说一下你测量的是什么 CPU,在方法上是否有任何重大差异?
  • Core 2 Q9550,6600 的 45 nm 版本。我接受了 243 的值,我知道我测量了很多废话
  • 根据ccsl.carleton.ca/~jamuir/rdtscpm1.pdf ,CPUID 有烦人的可变延迟。您可以尝试在没有 CPUID 的情况下重复。如果您的处理器支持,显然还有 RDTSCP 正在序列化。
  • @tc:感谢您的链接——我想我在其他地方找到了对那篇文章的引用,但它的链接已损坏。

标签: c++ performance assembly intel rdtsc


【解决方案1】:

RDTSC 可能由于多种原因返回不一致的结果:

  • 在某些 CPU(尤其是某些较旧的 Opterons)上,TSC 在内核之间不同步。听起来您已经使用 sched_setaffinity 处理了这个问题——很好!
  • 如果在您的代码运行时触发操作系统计时器中断,则会在运行时引入延迟。没有实际的方法可以避免这种情况。扔掉异常高的值。
  • CPU 中的流水线伪影有时会使您在紧密循环中的任一方向上偏离几个周期。完全有可能在非整数时钟周期内运行一些循环。
  • 缓存!根据 CPU 缓存的变幻莫测,内存操作(如写入times[])的速度可能会有所不同。在这种情况下,您很幸运,所使用的 std::vector 实现只是一个平面数组;即便如此,这种写法也会把事情搞砸。这可能是这段代码最重要的因素。

对于 Core2 微架构方面的专家,我还不足以确切说明为什么您会获得这种双峰分布,或者您的代码如何运行得更快 28 倍,但这可能与上述原因之一有关.

【讨论】:

  • 主要是体验。英特尔并不特别愿意提供其架构的细节,尤其是在内存和缓存访问的某些方面。
  • 看起来我只需要在不知道其根本原因的情况下接受差异(到目前为止,CPUID 或缓存效应是最可能的解释)。
  • Intel 的文档说 CPUID 前几次调用的时间较长,他们建议提前调用 3 次。此外,缓存几乎肯定是这里的一个因素,如果您想从其他基准测试中减去 rdtsc 的值,您应该只触及一个缓存行——使用单独的运行来获得平均值、最小值和最大值(只收集一个每次,并使用无分支的最小/最大实现)。最后,固定到一个核心不足以摆脱调度程序,您还需要防止其他进程在该核心上运行。
  • @OlofForshell:在 cpuid+rdtsc 调用之间仍然会有流水线。性能可能比“现实生活”要慢,但通常您正在进行此类测量,因此您可以比较两种选择,您关心的是一种是否比另一种相对更快。您不希望一个看起来比另一个相对快,因为由于微架构的某些工件,rdtsc 在您的第一个替代方案中的最后一条指令之前和您的第二个替代方案中的最后一条指令之后执行(第二条将受到阻碍)。跨度>
  • @OlofForshell:没有人会关心时钟周期,除非他们正在优化(在这种情况下,他们会比较两个替代方案以查看哪个更快)或者他们试图满足实时截止日期(在这种情况下,更悲观的答案是更好)。我的观点是,当使用 cpuid 比较两种替代方案的性能时,误导性较小——没有它,一个可能会比另一个更快,因为第二个 rdtsc 在您尝试测量的工作之前执行。如果你真的想要循环精度,你可以在最后减去 cpuid/rdtsc 开销,正如 Intel 建议的那样。
【解决方案2】:

英特尔程序员手册建议您使用lfence;rdtscrdtscp,以确保rdtsc 之前的指令已实际执行。这是因为rdtsc 本身并不是一个序列化指令。

【讨论】:

    【解决方案3】:

    您应确保在操作系统级别禁用频率限制/绿色功能。重新启动机器。否则,您可能会遇到内核具有不同步的时间戳计数器值的情况。

    243 读数是迄今为止最常见的读数,这也是使用它的原因之一。另一方面,假设您得到的经过时间

    我在这里的其余答案是我做什么,我如何处理结果以及我对主题的推理。

    至于开销,最简单的方法是使用这样的代码

    unsigned __int64 rdtsc_inline (void);
    unsigned __int64 rdtsc_function (void);
    

    第一种形式将 rdtsc 指令发送到生成的代码中(就像在您的代码中一样)。第二个将导致调用函数、执行 rdtsc 和返回指令。也许它会生成堆栈帧。显然第二种形式比第一种慢很多。

    然后可以编写用于开销计算的 (C) 代码

    unsigned __int64 start_cycle,end_cycle;    /* place these @ the module level*/
    
    unsigned __int64 overhead;
        
    /* place this code inside a function */
        
    start_cycle=rdtsc_inline();
      end_cycle=rdtsc_inline();
    overhead=end_cycle-start_cycle;
    

    如果您使用内联变体,您将获得较低的开销。您还将冒着计算大于“应该”的开销的风险(尤其是对于函数形式),这反过来意味着如果您测量非常短/快速的序列,您可能会遇到先前计算的开销,即大于测量本身。当您尝试调整开销时,您将得到一个下溢,这将导致混乱的情况。最好的处理方法是

    1. 多次计算开销,并始终使用达到的最小值,
    2. 不要测量非常短的代码序列,因为您可能会遇到流水线效应,这需要在 rdtsc 指令之前执行混乱的同步指令,并且
    3. 如果您必须测量非常短的序列,请将结果视为指示而不是事实

    我之前在this thread 中讨论过我如何处理结果。

    我要做的另一件事是将测量代码集成到应用程序中。开销是微不足道的。计算出结果后,我将其发送到一个特殊结构,在该结构中我计算测量次数,将 x 和 x^2 值相加并确定最小和最大测量值。稍后我可以使用这些数据来计算平均值和标准偏差。结构本身是索引的,我可以测量不同的性能方面,例如单个应用程序功能(“功能性能”)、cpu 花费的时间、磁盘读/写、网络读/写(“非功能性能”)等。

    如果以这种方式对应用程序进行检测并从一开始就对其进行监控,我希望它在其生命周期内出现性能问题的风险将大大降低。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-01-09
      • 2011-10-04
      • 2018-11-22
      • 2019-10-05
      • 2012-06-13
      • 1970-01-01
      • 2011-01-05
      • 1970-01-01
      相关资源
      最近更新 更多