定量分析是一切优化过程中最重要的过程之一。在性能优化和分析中,我们如何来描述一段代码的执行效率呢?这里,我将这一过程分为2步。
第一步:我们要找到一个客观的、相对稳定的单位,来描述一个程序的运行时间;
第二步:构造合理的、更接近起初情况的测试用例来进行测量。
这样,最终得到的结果,就是对于一段代码执行效率的客观描述。
一、单位确定
首先,我们要确定下来描述单位。通常来说,时间差被广泛地用做描述计算能力的单位。顾名思议,就是在代码前记下一个时间T1,在执行后记下一个时间T2。然后通过计算? = T2 - T1即可得到执行的时长。这种方法方便、快捷,而且也较为准确。它被广泛地用于对计算能力的约束上。如,暴力破解某某密码要几小时等等。相信大家都熟悉下面一段话:
“1976年9月,美国数学家阿佩尔和哈肯利用IBM360计算机工作了1200多个小时,终于证明了四色问题是正确的,轰动了世界。从此四色问题变成了四色定理。”
细心的读者一定已经发现,单纯的“1200多个小时”是不能够描述四色问题的验证过程时间的,它往往要跟上一个前提,在IBM360机器,要求有同型号、子型号、同频率、同批次的CPU,才可以得到相同的结果。那么,是否有更加客观的单位可以描述代码的执行能力呢?这个单位一定符合5个条件。
1)精度高——它不可能比时间精度低,通过gettimeofday得到的时间可以精确至“”;
2)时间相关——我们可以从它经过计算转换为相对时间:秒;
3)低耦合——尽量不要与CPU的主频等主要参数有过多的关系,换一台机器后它的值变化不会很大;
4)相对稳定——不会导致前后2次测量的变化过大。
CPU时钟周期,我们想到了这个基础单位。它的单位是Tick,对于主流的0.5GHz-2.0GHz的机器上,它分别可以精确至“”。我们可以将它与CPU主频进行相乘来得到相对时间。对于支持Pipeline、L1/L2 Cache,指令集相同的CPU,它的差别并不大。由于CPU周期是由硬件晶振控制的,在硬件不出故障情况下,它相对稳定。它完全符合精度高、时间相关、低耦合以及相对稳定4个特点。本文后续的讨论将全部基于CPU时间周期来进行讨论。
二、测量方法
我们已经确定了用纳秒级的计量单位“CPU时钟周期”。如何才能获得系统当前的CPU时钟周期呢?INTEL® CPU提供了一个64位的寄存器TSC来存储自开机来的周期值。并可通过指令“rdtsc”将其写入edx:eax。所以,我们可以在代码中插入这样的语句。
#define rdtscll_64(val) do {\ unsigned int __a,__d; __asm__ __volatile__("rdtsc" : "=a" (__a), "=d" (__d)); (val) = (((unsigned long long)__d)<<32) | (__a); } while(0);
通过调用rdtscll即可以得到64位的CPU时钟周期数。
三、测量误差
在了解测量方法之后,同样我们要了解到这个测量方法在哪些情况下会产生误差。
从TSC本身来说,最常见的误差便是多核的影响。CPU并不会对多个核的tsc进行同步,所以,我们首先要保证测量过程是在同一个核上进程的。如果被测代码段执行过程中存在进程切换,则测量值一定不会很准。所以如果我们在测量的过程中要保证被测量函数不要进行进程切换。也就是说如果被测函数有异步IO调用,我们则需要将进程绑定在一个核上。
由于cache的存在,导致我们在对访问密集型的代码进行测量的时候,前后2次测量的结果可能相差5-10倍,所以当我们希望获得单次非cache内内存获得开销的时候,我们需要需要取构造一个复杂的case。
时下非常流行的虚拟机技术也会影响测量的结果,因为有些虚拟机自己模拟了这个TSC这样我们拿到的值就是物理机硬件的tsc。因为在对虚拟机进行统计的时候需要关注它是如何获得TSC的。
当然,TSC本身也是有开销的,虽然它很小。大概在20-40个周期。
四、基本操作测量
1. 连续内存写操作
非常简单,只要memset一个大内存就OK了
2. 随机内存写操作
我们需要构造一个大的block_list,每2个相信节点间的距离都要大于 cache大小,且并不相等,前、后关系也不尽相同。同时,我们通过 memset一个和cache相同大小的buff来清空cache。便可以测量出随机内存写操作的开销。
3. 一些基本操作的开销
操作 |
平均周期数 |
操作 |
平均周期数 |
连续内存访问(4byte) |
10 |
浮点数值运算(+、-、*) |
10-12 |
随机内存访问(4byte) |
208 |
浮点数值运算(/) |
14 |
整数数值运算 |
7 |
系统随机 |
46 |
三角函数(sin/cos) |
135 |
五、范例:函数耗时分析
测试使用的CPU信息如下:
范例分别用CTscStat和CTimeStat类来测量funcA,funcB和funcC,用usleep来模拟真实函数的耗时。
代码说明:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <sys/time.h> #include <string> #include <vector> #include <stdint.h> #define rdtscll_64(val) do {\ unsigned int __a,__d; __asm__ __volatile__("rdtsc" : "=a" (__a), "=d" (__d)); (val) = (((unsigned long long)__d)<<32) | (__a); } while(0); class CTscStat { public: CTscStat(const std::string& sCallerName) : m_sCallerName(sCallerName) { rdtscll_64(m_start); rdtscll_64(m_lastcall); } ~CTscStat() { uint64_t end; rdtscll_64(end); uint64_t totalConsumeTsc = end - m_start; printf("TscStat---TOTAL:func=[%s] tsc_cost=[%lu]\n", m_sCallerName.c_str(), totalConsumeTsc); } void AddCall(const std::string& sCall) { AddCall(sCall, GetTscSub()); } private: void AddCall(const std::string& sCallerName, uint64_t consumeTsc) { printf("TscStat---SubCall:func=[%s] sub_func=[%s] tsc_cost=[%lu]\n", m_sCallerName.c_str(), sCallerName.c_str(), consumeTsc); rdtscll_64(m_lastcall); } uint64_t GetTscSub() { uint64_t now; rdtscll_64(now); return now - m_lastcall; } private: struct SCall { std::string sCallerName; uint64_t consumeTsc; }; private: std::string m_sCallerName; // 统计接口 std::vector<SCall> m_vecCall; // 请求列表 uint64_t m_start; // 开始TSC uint64_t m_lastcall; // 上次TSC }; #define TSC_START(sCn) CTscStat oTscS(sCn); #define TSC_APICALL(API) do { oTscS.AddCall(API); } while(0) class CTimeStat { public: CTimeStat(const std::string& sCallerName) : m_sCallerName(sCallerName) { gettimeofday(&m_start, 0); gettimeofday(&m_lastcall, 0); } ~CTimeStat() { struct timeval end; struct timeval subresult; gettimeofday(&end, 0); timersub(&end, &m_start, &subresult); uint32_t dwTotalConsumeTime = subresult.tv_sec * 1000 + subresult.tv_usec / 1000; printf("TimeStat---TOTAL:func=[%s] time_cost=[%u]\n", m_sCallerName.c_str(), dwTotalConsumeTime); } void AddCall(const std::string& sCall) { AddCall(sCall, GetTimeSub()); } private: void AddCall(const std::string& sCallerName, uint32_t dwConsumeTime) { printf("TimeStat---SubCall:func=[%s] sub_func=[%s] time_cost=[%u]\n", m_sCallerName.c_str(), sCallerName.c_str(), dwConsumeTime); gettimeofday(&m_lastcall, 0); } uint32_t GetTimeSub() { struct timeval now; struct timeval subresult; gettimeofday(&now, 0); timersub(&now, &m_lastcall, &subresult); return subresult.tv_sec * 1000 + subresult.tv_usec / 1000; } private: struct SCall { std::string sCallerName; uint32_t dwConsumeTime; }; private: std::string m_sCallerName; // 统计接口 std::vector<SCall> m_vecCall; // 请求列表 struct timeval m_start; // 开始时间 struct timeval m_lastcall; // 上次请求时间 }; #define TS_START(sCn) CTimeStat oTimeS(sCn); #define TS_APICALL(API) do { oTimeS.AddCall(API); } while(0) void funcA() { usleep(1000); // 1ms } void funcB() { usleep(100000); // 100ms } void funcC() { sleep(1); // 1s } void TestTsc() { TSC_START("TestTsc"); funcA(); TSC_APICALL("funcA"); funcB(); TSC_APICALL("funcB"); funcC(); TSC_APICALL("funcC"); } void TestTime() { TS_START("TestTsc"); funcA(); TS_APICALL("funcA"); funcB(); TS_APICALL("funcB"); funcC(); TS_APICALL("funcC"); } int main(int argc, char** argv) { ////////// tsc /////////// TestTsc(); ///////// time /////////// TestTime(); return 0; }
结果说明:
[[email protected] linux_programming]# ./module_perform
TscStat---SubCall:func=[TestTsc] sub_func=[funcA] tsc_cost=[6391184]
TscStat---SubCall:func=[TestTsc] sub_func=[funcB] tsc_cost=[362433912]
TscStat---SubCall:func=[TestTsc] sub_func=[funcC] tsc_cost=[3595441050]
TscStat---TOTAL:func=[TestTsc] tsc_cost=[3964662011]
TimeStat---SubCall:func=[TestTsc] sub_func=[funcA] time_cost=[2]
TimeStat---SubCall:func=[TestTsc] sub_func=[funcB] time_cost=[100]
TimeStat---SubCall:func=[TestTsc] sub_func=[funcC] time_cost=[1000]
TimeStat---TOTAL:func=[TestTsc] time_cost=[1104]
可以看到,当sleep 1ms的时候,测试结果与预期有较大差距,这个跟系统的精度有关。
当sleep 1s 和100ms的时候用Tsc测量的结果分别是主频和主频的1/10。