问题描述
|
我在尝试在某些原始图像处理操作中使用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)
我的问题是:
上面的代码生成的周期计数的双峰分布的可能原因是什么?
为什么最快的时间(234个周期)只发生少数几次—在极不寻常的情况下可以减少计数吗?
更多信息
平台:
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
解决方法
RDTSC
可能由于多种原因而返回不一致的结果:
在某些CPU(尤其是某些较旧的Opterons)上,TSC在内核之间不同步。听起来您已经通过使用sched_setaffinity
处理了-太好了!
如果在代码运行时触发OS计时器中断,则会在运行时引入延迟。没有实际的方法可以避免这种情况;只是抛出了异常高的价值。
在CPU中进行流水线化处理有时可能会使您在紧密循环中向任一方向倾斜几个周期。完全有可能使某些循环以非整数个时钟周期运行。
快取!根据CPU缓存的变化情况,内存操作(如写入“ 7”)的速度可能会有所不同。在这种情况下,您很幸运正在使用的std::vector
实现只是一个平面数组。即便如此,这种写法也能使事情顺利进行。这可能是此代码最重要的因素。
我对Core2微体系结构的专家还不够,不能确切地说出为什么要获得这种双峰分布,或者代码运行速度快了28倍,但这可能与上述原因之一有关。
, 如果您要确保确实执行了rdtsc
之前的指令,则英特尔程序员手册建议您使用lfence;rdtsc
或rdtscp
。这是因为rdtsc
本身并不是序列化指令。
,您应确保在操作系统级别禁用了频率调节/绿色功能。重新启动机器。否则,您可能会遇到内核具有不同步的时间戳计数器值的情况。
到目前为止,243读数是最常见的,这是使用它的原因之一。另一方面,假设您的经过时间小于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;
如果您使用的是内联变体,您将获得较低的开销。您还冒着计算开销的风险,该开销大于“应该”(特别是对于函数形式),这反过来意味着,如果您测量非常短/快速的序列,则可能会遇到先前计算的开销大于测量本身。当您尝试调整开销时,会出现下溢情况,这将导致混乱的情况。最好的解决方法是
将开销数倍,并始终使用获得的最小值,
不要测量很短的代码序列,因为您可能会遇到流水线化的效果,这需要在rdtsc指令和
如果您必须测量非常短的序列,则将结果视为指示,而不是事实
之前,我已经讨论了如何处理该线程中的结果。
我要做的另一件事是将测量代码集成到应用程序中。开销微不足道。计算完结果后,我将其发送到一个特殊的结构,在其中计算测量次数,将x和x ^ 2值相加并确定最小和最大测量值。稍后,我可以使用数据来计算平均值和标准偏差。结构本身已建立索引,我可以测量不同的性能方面,例如各个应用程序功能(“功能性能”),在cpu上花费的时间,磁盘读/写,网络读/写(“非功能性能”)等。
如果以这种方式对应用程序进行检测并从一开始就对其进行监视,我预计它将大大降低其在生命周期内出现性能问题的风险。