RDTSC管理费用的差异

Variance in RDTSC overhead

本文关键字:管理费用 RDTSC      更新时间:2023-10-16

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

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

extern inline unsigned long long __attribute__((always_inline)) rdtsc64() {
    unsigned int hi, lo;
        __asm__ __volatile__(
            "xorl %%eax, %%eaxnt"
            "cpuidnt"
            "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

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

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

我还不足以成为Core2微体系结构的大师,无法确切地说出为什么你会得到这种双峰分布,或者你的代码是如何运行得快28倍的,但这可能与上述原因之一有关。

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

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

243的读数是迄今为止最常见的,这也是使用它的原因之一;243:你减去开销,得到下溢。由于算术是无符号的,你最终会得到一个巨大的结果。这个事实说明使用最低读数(234)。精确测量只有几个周期长的序列是极其困难的。在几GHz的典型x86上,我建议不要使用短于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;

如果您使用内联变体,您将获得较低的开销。您还将面临计算大于它的开销的风险";应该";be(尤其是对于函数形式),这反过来意味着,如果你测量非常短/快速的序列,你可能会遇到之前计算的开销大于测量本身。当你试图调整头顶时,你会遇到下溢,这将导致混乱的情况。处理此问题的最佳方法是

  1. 将开销计时几次,并且总是使用所获得的最小值
  2. 不要测量真正短的代码序列,因为你可能会遇到流水线效应,这将需要在rdtsc指令之前混乱的同步指令
  3. 如果必须测量很短的序列,则将结果视为指示而非事实

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

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

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