优化准则:
1.二八法则:在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%的尽管是多数,却是次要的;在优化实践中,我们将精力集中在优化那20%最耗时的代码上,整体性能将有显著的提升;这个很好理解。函数A虽然代码量大,但在一次正常执行流程中,只调用了一次。而另一个函数B代码量比A小很多,但被调用了1000次。显然,我们更应关注B的优化。
2.编完代码,再优化;编码的时候总是考虑最佳性能未必总是好的;在强调最佳性能的编码方式的同时,可能就损失了代码的可读性和开发效率;
工具:
1Gprof
工欲善其事,必先利其器。对于Linux平台下C++的优化,我们使用gprof工具。gprof是GNUprofile工具,可以运行于linux、AIX、Sun等操作系统进行C、C++、Pascal、Fortran程序的性能分析,用于程序的性能优化以及程序瓶颈问题的查找和解决。通过分析应用程序运行时产生的“flatprofile”,可以得到每个函数的调用次数,消耗的CPU时间(只统计CPU时间,对IO瓶颈无能为力),也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函数调用花费了多少时间。
2.gprof使用步骤
1) 用gcc、g++、xlC编译程序时,使用-pg参数,如:g++-pg -o test.exetest.cpp编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序运行时采集并记录函数的调用关系和调用次数,并记录函数自身执行时间和被调用函数的执行时间。
2) 执行编译后的可执行程序,如:./test.exe。该步骤运行程序的时间会稍慢于正常编译的可执行程序的运行时间。程序运行结束后,会在程序所在路径下生成一个缺省文件名为gmon.out的文件,这个文件就是记录程序运行的性能、调用关系、调用次数等信息的数据文件。
3) 使用gprof命令来分析记录程序运行信息的gmon.out文件,如:gproftest.exe gmon.out则可以在显示器上看到函数调用相关的统计、分析信息。上述信息也可以采用gproftest.exe gmon.out>gprofresult.txt重定向到文本文件以便于后续分析。
以上只是gpro的使用步骤简介,关于gprof使用实例详见附录1;
实践
我们的程序遇到了性能瓶颈,在采用架构改造,改用内存数据库之前,我们考虑从代码级入手,先尝试代码级的优化;通过使用gprof分析,我们发现以下2个最为突出的问题:
1.初始化大对象耗时
分析报告:307 6.5%VOBJ1::VOBJ1@240038VOBJ1
在整个执行流程中被调用307次,其对象初始化耗时占到6.5%。
这个对象很大,包含的属性多,属于基础数据结构;
在程序进入构造函数函数体之前,类的父类对象和所有子成员变量对象已经被生成和构造。如果在构造函数体内位其执行赋值操作,显示属于浪费。如果在构造函数时已经知道如何为类的子成员变量初始化,那么应该将这些初始化信息通过构造函数的初始化列表赋予子成员变量,而不是在构造函数函数体中进行这些初始化。因为进入构造函数函数体之前,这些子成员变量已经初始化过一次了。
在C++程序中,创建/销毁对象是影响性能的一个非常突出的操作。首先,如果是从全局堆中生成对象,则需要首先进行动态内存分配操作。众所周知,动态分配/回收在C/C++程序中一直都是非常耗时的。因为牵涉到寻找匹配大小的内存块,找到后可能还需要截断处理,然后还需要修改维护全局堆内存使用情况信息的链表等。
解决方法:我们将大部分的初始化操作都移到初始化列表中,性能消耗降到1.8%。
2.Map使用不当
分析报告:89 6.8%Recordset::GetField
Recordset的getField被调用了89次,性能消耗占到6.8%;
Recordset是我们在在数据库层面的包装,对应取出数据的记录集;(用过ADO的朋友很熟悉);由于我们使用的是底层c++数据库接口,通过对数据库原始api进行一层包装,从而屏蔽开发人员对底层api的直接操作。这样的包装,带来的好处就是不用直接与底层数据库交互,在代码编写方面方便不少,代码可读性也很好;带来的问题就是性能的损失;
分析:(2点原因)
1)在GetField函数中,使用了map[“a”]来查询数据,如果找不到“a”,则map会自动插入key”a”,并设value为0;而m.find(“a”)不会自动插入上述pair,执行效率更高;原有逻辑:
string Recordset::GetField(const string &strName)
{
int nIndex;
if (hasIndex==false)
{
nIndex =m_nPos;
}
else
{
nIndex =m_vSort[m_nPos].m_iorder;
}
if (m_fields[strName]==0)
{
LOG_ERR("Recordset::GetField:"<
return m_records[nIndex].GetValue(m_fields[strName] - 1);
}
改造后的逻辑:
string Recordset::GetField(const string &strName)
{
unordered_map::iterator iter = m_fields.find(strName);
if (iter == m_fields.end())
{
LOG_ERR("[Recordset::GetField] "<< strName
调整后的Recordset::GetField的执行时间约是之前的1/2;且易读性更高;
2)在Recordset中,对于每个字段的存储,使用的是mapm_fields; g++中的stl标准库中默认使用的红黑树作为map的底层数据结构;
通过附录中的文档2,我们发现其实有更快的结构,在效率上,unordermap优于hashmap, hash map 优于红黑树;如果不要求map有序,unordered_map是更好的选择;
解决方法:将map结构换成unordered_map,性能消耗降到1.4%;
总结
我们修改不到30行代码,整体性能提升10%左右,效果明显;打蛇打七寸,性能优化的关键在于找准待优化的点,之后的事,也就水到渠成;
bythe way,对于Linux平台使用C++工作的朋友,推荐一本好书:《程序员的自我修养》。这本书介绍了运行库相关的各种技术。对装载、链接和库进行了深入浅出的剖析。看过真是大呼过瘾;
附录:
附1:prof工具介绍及实践
附2:maphash_map unordered_map性能测试
二、++i和i++引申出的效率问题
看了上面的第一点,你可能觉得,那不就是多调用了四个函数而已,你可能对此不屑一顾。那么来看看下面的例子,应该会让你大吃一惊。
至于整型变量的前加和后加的区别相信大家也是很清楚的。然而在这里我想跟大家谈的却是C++类的运算符重载,为了与整形变量的用法一致,在C++中重载运算符++时一般都会把前加和后加都重载。你可能会说,你在代码中不会重载++运算符,但是你敢说你没有使用过类的++运算符重载吗?迭代器类你总使用过吧!可能到现在你还不是很懂我在说什么,那么就先看看下面的例子吧,是本人为链表写的一个内部迭代器。
1
2
3
4
5
6
7
8
9
10
11
|
_SingleList::Iterator& _SingleList::Iterator::operator++()
//前加
{
pNote = pNote->pNext;
return
*
this
;
}
_SingleList::Iterator _SingleList::Iterator::operator++(
int
)
//后加
{
Iterator tmp(*
this
);
pNote = pNote->pNext;
return
tmp;
}
|
从后加的实现方式可以知道,对象利用自己创建一个临时对象(自己在函数调用的一个复制),然后改变自己的状态,并返回这个临时对象,而前加的实现方式时,直接改变自己的内部状态,并返回自己的引用。
从第一点的论述可以知道后加实现时会调用复制构造函数,在函数返回时还要调用析构函数,而由于前加实现方式直接改变对象的内部状态,并返回自己的引用,至始至终也没有创建新的对象,所以也就不会调用构造函数和析构函数。
然而更加糟糕的是,迭代器通常是用来遍历容器的,它大多应用在循环中,试想你的链表有100个元素,用下面的两种方式遍历:
1
2
3
4
5
6
7
8
9
|
for
(_SingleList::Iterator it = list.begin(); it != list.end(); ++it)
{
//do something
}
for
(_SingleList::Iterator it = list.begin(); it != list.end(); it++)
{
//do something
}
|
如果你的习惯不好,写了第二种形式,那么很不幸,做同样的事情,就是因为一个前加和一个后加的区别,你就要调用多200个函数,其对效率的影响可就不可忽视了。
参考:http://blog.chinaunix.net/uid-29068482-id-3870978.html