为了保证测量的准确,我们需要尽可能排除其他因素的干扰,一般我们需要注意如下的几点:
确保没有其他的进程在运行,关闭后台进程和定时任务。
断开网络,甚至不要碰鼠标:因为这些外界影响会引起操作系统的中断。
被测量的程序最好不要在 CPU 第 0 个核上运行:因为第 0 个核通常会用于处理中断。
不要打开超线程:超线程技术可以将一个核心虚拟化成两个核心,而在软件层面上感知不到。而虚拟化出来的两个核心性能并非等同于真正的两个核心,这会给我们的测量带来麻烦。
关闭 DVFS:DVFS 使得当 CPU 过热时自动降低频率来控制温度。
关闭 Turbo Boost:Turbo Boost 使得,如果 CPU 其他核心负载较小,只有一个核心在执行较多任务时,提高该核心的频率。
使用 taskset
这个工具,来把指定进程绑定到 CPU 的某个核心上。
除此以外,我们的程序最好是满足对齐要求的(编译器应该已经解决了这点)。不然的话,可能我们的程序进行细微改动之后,被改动地方的后面的位置都向前或是向后平移,有些数据原来可能只需要从内存中读一次就能读完,现在需要读两次才能读完,影响了性能。
有意思的是可能可执行文件的名称的长度也会影响性能。因为操作系统加载可执行文件之后,会在栈上加入命令行参数,第一个参数就是可执行文件的名称。名称长度发生变化就会使得栈上后续内存的对齐出现问题。
还有更玄学的。DRAM 是容易受到外部环境干扰,使得存储的位发生翻转的。但计算机有一些机制来进行纠错,而纠错的过程就会带来一些时间上的影响。
我们可以直接使用 /usr/bin/time
进行测量。该测量程序运行完毕后会给出三个时间:
一般 real time 约等于 user time + sys time。
但是有可能出现 IO 等待、线程堵塞(如等待锁)、操作系统调度的开销等因素,使得 real time 偏大。
由于 sys time 和 user time 是会在多核上累积的,所以如果程序是多核并行的话,real time 又会偏小。
X86 处理器提供一个 time-stamp counter (TSC)。存储的是 CPU 自通电以来经过的周期数量。我们可以用如下的代码进行读取:
不过一般不建议使用该方法,因为其有如下的缺点:
gettimeofday()
也不建议使用。计算机内部会存储并更新一个时间,但一般会有偏差,使得与现实世界的时间的差距越来越大。而操作系统又会定期联网同步时间,当时间同步的时候可能会使得系统的时间发生变化(应该不是突然改变,而是加速向真实时间靠近)。而 gettimeofday
就依赖于系统的时间,因此并不可靠。
最推荐使用的是 clock_time(CLOCK_MONOTONIC, ...)
。
CLOCK_MONOTONIC
参数可以确保得到的时间一定是单调递增的,不会像 rdtsc
一样由于溢出而回滚,或是 gettimeofday
一样受系统时间同步的影响。
并且 clock_gettime
貌似在系统层面有优化,要比普通的系统调用快一点。
如果我们想知道程序中哪个函数运行的最慢,我们可以用这样的方法进行粗略的测量:使用 gdb
运行程序,然后固定间隔按一下 Ctrl+C ,就知道当前正在运行的函数。通过多次采样就知道程序中哪个函数所占时间最长了。
pmprof
和 gprof
就是这个原理,并且他们已经把这个过程自动化了,不需要再手动操作。
CPU 中有一个可编程的 PMU,可以对指定事件进行计数(如缓存未命中,分支预测错误等)。当我们需要统计某个事件的时候,可以向 PMU 注册这个事件。后续当这个事件发生之后,PMU 就会将对应的计数器加一。
libpfm4
提供了对应的接口。perf stat
就是利用 libpfm4
实现的。
值得注意的是,PMU 内的计数器是有限的(大概是 4 到 5 个的样子)。考虑到可能同时有多个进程有对多个事件进行统计的要求,操作系统会采用类似分时复用的机制将 PMU 进行虚拟化。比建议同时测量超过 4 到 5 个事件,因为这样会降低准确性和性能。
我们可以使用模拟器来精准且可复现地测量程序性能,如 cachegrind
可以模拟缓存情况(在一定程度上)。但模拟器一般速度比较慢。