C++的性能优化实践
优化准则:
1. 二八法则:在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%的尽管是多数,却是次要的;在优化实践中,我们将精力集中在优化那20%最耗时的代码上,整体性能将有显著的提升;这个很好理解。函数A虽然代码量大,但在一次正常执行流程中,只调用了一次。而另一个函数B代码量比A小很多,但被调用了1000次。显然,我们更应关注B的优化。
2. 编完代码,再优化;编码的时候总是考虑最佳性能未必总是好的;在强调最佳性能的编码方式的同时,可能就损失了代码的可读性和开发效率;
工具:
1 Gprof
工欲善其事,必先利其器。对于Linux平台下C++的优化,我们使用gprof工具。gprof是GNU profile工具,可以运行于linux、AIX、Sun等操作系统进行C、C++、Pascal、Fortran程序的性能分析,用于程序的性能优化以及程序瓶颈问题的查找和解决。通过分析应用程序运行时产生的“flat profile”,可以得到每个函数的调用次数,消耗的CPU时间(只统计CPU时间,对IO瓶颈无能为力),也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函数调用花费了多少时间。
2. gprof使用步骤
1) 用gcc、g++、xlC编译程序时,使用-pg参数,如:g++ -pg -o test.exe test.cpp编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序运行时采集并记录函数的调用关系和调用次数,并记录函数自身执行时间和被调用函数的执行时间。
2) 执行编译后的可执行程序,如:./test.exe。该步骤运行程序的时间会稍慢于正常编译的可执行程序的运行时间。程序运行结束后,会在程序所在路径下生成一个缺省文件名为gmon.out的文件,这个文件就是记录程序运行的性能、调用关系、调用次数等信息的数据文件。
3) 使用gprof命令来分析记录程序运行信息的gmon.out文件,如:gprof test.exe gmon.out则可以在显示器上看到函数调用相关的统计、分析信息。上述信息也可以采用gprof test.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,执行效率更高;原有逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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:" <<strName<< " Not Find!!" ); } return m_records[nIndex].GetValue(m_fields[strName] - 1) ; } |
改造后的逻辑:
1
2
3
4
5
6
7
|
string Recordset::GetField( const string &strName) { unordered_map::iterator iter = m_fields.find(strName); if (iter == m_fields.end()) { LOG_ERR( "[Recordset::GetField] " << strName <second - 1) ; } |
调整后的Recordset::GetField的执行时间约是之前的1/2;且易读性更高;
2)在Recordset中,对于每个字段的存储,使用的是map m_fields; g++中的stl标准库中默认使用的红黑树作为map的底层数据结构;
通过附录中的文档2,我们发现其实有更快的结构, 在效率上,unorder map优于hash map, hash map 优于 红黑树;如果不要求map有序,unordered_map 是更好的选择;
解决方法:将map结构换成unordered_map,性能消耗降到1.4%;
总结
我们修改不到30行代码,整体性能提升10%左右,效果明显;打蛇打七寸,性能优化的关键在于找准待优化的点,之后的事,也就水到渠成;
附录:
附1:prof工具介绍及实践
附2: map hash_map unordered_map 性能测试