c++性能优化技术导论<一>

2015-01-24 05:56:37 · 作者: · 浏览: 4

?

by?冲出宇宙

2011-8-24

?

1、性能优化原理

??????? 在谈论性能优化技术之前,有几点大家一定要明确。第一点是必须有编写良好的代码,编写的很混乱的代码(如注释缺乏、命名模糊),很难进行优化。第二点是良好的构架设计,性能优化只能优化单个程序,并不能够优化蹩脚的构架。不过,网络如此发达,只要不是自己乱想的构架,只要去积极分析别人的成功构架,大家几乎不会遇到蹩脚的构架。

?

?

?

1.1、计算函数、代码段调用次数和耗时

??????? 函数的调用次数比较好说,用一个简单的计数器即可。一个更加通用的框架可能是维护一个全局计数,每次进入函数或者代码段的时候,给存储的对应计数增加1

??????? 为了精确的计算一段代码的耗时,我们需要极高精度的时间函数。gettimeofday是其中一个不错的选择,它的精度在1us,每秒可以调用几十万次。注意到现代cpu每秒能够处理上G的指令,所以1uscpu可以处理几千甚至上万条指令。对于代码长度少于百行的函数来说,其单次执行时间很可能小于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保存了耗时

?

//?得到cputick?countcpuid(重整时钟周期)消耗约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?

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系统上面最常见。callgrindvalgrind工具里面的一员,它的主要功能是模拟cpucache,能够计算多级cache的有效、失效次数,以及每个函数的调用耗时统计。

callgrind的实现机理(基于外部中断)决定了它有不少缺点。比如,会导致程序严重变慢、不支持高度优化的程序、耗时统计的结果误差较大等等。

?

?

我们编写了一个简单的测试程序,用它来测试常见性能分析工具。代码如下:

//?计算最大公约数

inline?int?gcd(int?m,?int?n)

{

PERFOMANCE("gcd");?//?全局计算耗时的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?

g?+=?gcd(1234134,idx);

uint64_t?time?=?GetTickCPU()?-?pretime;

?

printf("%d,%lld\n",?g,?time);

return?0;

}

?

callgrind运行的结果如下:

?

?

?

我们把输出的结果在windows下用callgrind的工具分析,得到如下结果:

?

?

?

1.4g++性能分析

gprofg++自带的性能分析工具(gnu?profile)。它通过内嵌代码到各个函数里面来计算函数耗时。按理说它应该对高度优化代码很有效,但实际上它对-O2的代码并不友好,这个可能和它的实现位置有关系(在代码优化之后)。gprof的原理决定了它对程序影响较小。

?

?

下图是同样的程序,用gprof检查的结果:

?

?

我们可以看到,这个结果比callgrind计算的要精确很多