RDTSC开销的差异

时间:2011-06-21 22:15:16

标签: c++ performance assembly intel rdtsc

我正在构建一个微基准来测量性能变化,因为我在一些原始图像处理操作中尝试使用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个周期)只发生了几次 - 非常不寻常的环境可以减少计数?

  3. 更多信息

    平台:

    • Linux 2.6.32(Ubuntu 10.04)
    • g ++ 4.4.3
    • Core 2 Duo(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
    

3 个答案:

答案 0 :(得分:7)

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

  • 在某些CPU(特别是某些较旧的Opteron)上,TSC在内核之间不同步。听起来你已经使用sched_setaffinity - 好了!
  • 来处理这个问题
  • 如果在代码运行时OS定时器中断触发,则运行时会引入延迟。没有切实可行的方法来避免这种情况;只是扔出异乎寻常的高价值。
  • CPU中的流水线工件有时会在紧密循环中向任何方向抛出几个周期。完全可能有一些循环以非整数个时钟周期运行。
  • 缓存!根据CPU缓存的变化,内存操作(如写入times[])的速度可能会有所不同。在这种情况下,您很幸运,正在使用的std::vector实现只是一个扁平数组;即便如此,这种写作也会让事情发生。这可能是此代码最重要的因素。

我对Core2微体系结构的大师来说,还不足以说明为什么你会得到这种双峰分布,或者你的代码如何更快地运行28次,但它可能与上述原因之一有关

答案 1 :(得分:2)

如果您想确保lfence;rdtsc之前的指示已实际执行,则英特尔程序员手册建议您使用rdtscprdtsc。这是因为rdtsc本身不是序列化指令。

答案 2 :(得分:1)

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

243读数是迄今为止最常见的,这是使用它的一个原因。另一方面,假设您得到一个经过的时间&lt; 249:您减去开销并获得下溢。由于算术是无符号的,因此最终得到了巨大的结果。这个事实代表使用最低读数(243)。准确测量只有几个周期的序列是非常困难的。在典型的x86 @几GHz上,我建议不要使用短于10ns的时序,即使在那个长度,它们通常也会远离坚如磐石。

我在这里的其余部分是我的工作,我如何处理结果以及我对主题的推理。

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

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. 如果您必须测量非常短的序列,请将结果视为指示而非事实
  4. 我之前已经在this thread讨论了我对结果的处理方式。

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

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