c++性能优化技术导论<一>
by 冲出宇宙
2011-8-24
1、性能优化原理
在谈论性能优化技术之前,有几点大家一定要明确。第一点是必须有编写良好的代码,编写的很混乱的代码(如注释缺乏、命名模糊),很难进行优化。第二点是良好的构架设计,性能优化只能优化单个程序,并不能够优化蹩脚的构架。不过,网络如此发达,只要不是自己乱想的构架,只要去积极分析别人的成功构架,大家几乎不会遇到蹩脚的构架。
1.1、计算函数、代码段调用次数和耗时
函数的调用次数比较好说,用一个简单的计数器即可。一个更加通用的框架可能是维护一个全局计数,每次进入函数或者代码段的时候,给存储的对应计数增加1。
为了精确的计算一段代码的耗时,我们需要极高精度的时间函数。gettimeofday是其中一个不错的选择,它的精度在1us,每秒可以调用几十万次。注意到现代cpu每秒能够处理上G的指令,所以1us内cpu可以处理几千甚至上万条指令。对于代码长度少于百行的函数来说,其单次执行时间很可能小于1us。目前最精确的计时方式是cpu自己提供的指令:rdtsc。它可以精确到一个时钟周期(1条指令需要消耗cpu几个时钟周期)。
我们注意到,系统在调度程序的时候,可能会把程序放到不同的cpu核心上面运行,而每个cpu核心上面运行的周期不同,从而导致了采用rdtsc时,计算的结果不正确。解决方案是调用linux系统的sched_setaffinity来强制进程只在固定的cpu核心上运行。
有关耗时计算的参考代码:
// 通常计算代码耗时
uint64_t preTime = GetTime();
//代码段
uint64_t timeUsed = GetTime() - preTime;
// 改进的计算方式
struct TimeHelper{
uint64_t preTime;
TimeHelper():preTime(GetTime())
{}
~TimeHelper(){
g_timeUsed = GetTime() - preTime;
}
};
// 调用
{
TimeHelper th;
// 代码段
}
// g_timeUsed保存了耗时
// 得到cpu的tick count,cpuid(重整时钟周期)消耗约300周期(如果不需要特别精确的精度,可以不执行cpuid
inline uint64_t GetTickCPU()
{
uint32_t op; // input: eax
uint32_t eax; // output: eax
asm volatile(
"pushl %%ebx \n\t"
"cpuid \n\t"
"popl %%ebx \n\t"
: "=a"(eax) : "a"(op) : "cc" );
uint64_t ret;
asm volatile ("rdtsc" : "=A" (ret));
return ret;
}
// 得到cpu的主频, 本函数第一次调用会耗时0.01秒钟
inline uint64_t GetCpuTickPerSecond()
{
static uint64_t ret = 0;
if(ret == 0)
{
const uint64_t gap = 1000000 / 100;
uint64_t endTime = GetTimeUS() + gap;
uint64_t curTime = 0;
uint64_t tickStart = GetTickCPU();
do{
curTime = GetTimeUS();
}while(curTime < endTime);
uint64_t tickCount = GetTickCPU() - tickStart;
ret = tickCount * 1000000L / (curTime - endTime + gap);
}
return ret;
}
1.2、其他策略
除了基本的计算执行次数和时间外,还有如下几种分析性能的策略:
a、基于概率
通过不断的中断程序,查看程序中断的位置所在的函数,出现次数最多的函数即为耗时最严重的函数。
b、基于事件
当发生一次cpu硬件事件的时候,某些cup会通知进程。如果事件包括L1失效多少次这种,我们就能知道程序跑的慢的原因。
c、避免干扰
性能测试最忌讳外界干扰。比如,内存不足,读内存变成了磁盘操作。
1.3、性能分析工具-callgrind
valgrind系列工具因为免费,所以在linux系统上面最常见。callgrind是valgrind工具里面的一员,它的主要功能是模拟cpu的cache,能够计算多级cache的有效、失效次数,以及每个函数的调用耗时统计。
callgrind的实现机理(基于外部中断)决定了它有不少缺点。比如,会导致程序严重变慢、不支持高度优化的程序、耗时统计的结果误差较大等等。
我们编写了一个简单的测试程序,用它来测试常见性能分析工具。代码如下:
// 计算最大公约数
inline int 易做图(int m, int n)
{
PERFOMANCE("易做图"); // 全局计算耗时的define
int d = 0;
do{
d = m % n;
m = n;
n = d;
}while(d > 0);
return m;
}
// 主函数
int main(){
int g = 0;
uint64_t pretime = GetTickCPU();
for(int idx = 1; idx < 1000000;idx ++)
g += 易做图(1234134,idx);
uint64_t time = GetTickCPU() - pretime;
printf("%d,%lld\n", g, time);
return 0;
}
callgrind运行的结果如下:
我们把输出的结果在windows下用callgrind的工具分析,得到如下结果:
1.4、g++性能分析
gprof是g++自带的性能分析工具(gnu profile)。它通过内嵌代码到各个函数里面来计算函数耗时。按理说它应该对高度优化代码很有效,但实际上它对-O2的代码并不友好,这个可能和它的实现位置有关系(在代码优化之后)。gprof的原理决定了它对程序影响较小。
下图是同样的程序,用gprof检查的结果:
我们可以看到,这个结果比callgrind计算的要精确很多
补充:软件开发 , C++ ,