15

当我在一些原始图像处理操作中尝试使用 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
4

3 回答 3

10

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

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

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

于 2011-06-21T22:39:56.463 回答
3

The Intel Programmer's manual recommends you use lfence;rdtsc or rdtscp if you want to ensure that instructions prior to the rdtsc have actually executed. This is because rdtsc isn't a serializing instruction by itself.

于 2011-12-14T08:46:30.967 回答
1

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

到目前为止,243 读数是最常见的,这也是使用它的原因之一。另一方面,假设您得到的经过时间<243:减去开销并得到下溢。由于算术是无符号的,你最终会得到一个巨大的结果。这个事实说明使用最低读数(234)代替。精确测量只有几个周期长的序列是极其困难的。在典型的 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. 如果您必须测量非常短的序列,请将结果视为指示而不是事实

我之前已经讨论过我如何处理这个线程中的结果。

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

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

于 2011-08-17T12:49:45.203 回答