gprof 安装在Linux 系统的 /usr/bin 目录下. 它能剖析你的程序,并分析出程序的哪一个部分在执行时最费时间.
gprof 将告诉你程序里每个函数被调用的次数和每个函数执行时所占时间的百分比. 你如 果想提高你的程序性能的话这些信息非常有用.
为了在你的程序上使用 gprof, 你必须在编译程序时加上 -pg 选项. 这将使程序在每次 执行时产生一个叫 gmon.out 的文件. gprof 用这个文件产生剖析信息.在你运行了你的程序并产生了 gmon.out 文件后你能用下面的命令获得剖析信息:
gprof <program_name>
参数 program_name 是产生 gmon.out 文件的程序的名字. 为了说明问题,在程序中增加了函数count_sum()以消耗CPU时间,程序如下
#include <stdio.h>
static void my_print (char *);
static void my_print2 (char *);
main ()
{
char my_string[] = "hello world!";
my_print (my_string);
my_print2 (my_string);
my_print (my_string);
}
void count_sum()
{
int i,sum=0;
for(i=0; i<1000000; i++)
sum += i;
}
void my_print (char *string)
{
count_sum();
printf ("The string is %s ", string);
}
void my_print2 (char *string)
{
char *string2;
int size, i,sum =0;
count_sum();
size = strlen (string);
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++) string2[size -1 - i] = string[i];
string2[size] = '';
for(i=0; i<5000000; i++)
sum += i;
printf ("The string printed backward is %s ", string2);
}
$ gcc -pg -o hello hello.c
$ ./hello
$ gprof hello | more
将产生以下的输出
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total time seconds seconds calls us/call us/call name
69.23 0.09 0.09 1 90000.00 103333.33 my_print2
30.77 0.13 0.04 3 13333.33 13333.33 count_sum
0.00 0.13 0.00 2 0.00 13333.33 my_print
由以上数据可以看出,执行my_print()函数本身没花费什么时间,但是它又调用了
count_sum()函数,所以累计秒数为0.13.
技巧: gprof 产生的剖析数据很大, 如果你想检查这些数据的话最好把输出重定向到一个 文件里。
.
软件性能由两大因素决定,如上图。软件设计是语言无关的,设计者必须充分了解软件的主题功能,设计不良造成的性能问题不要指望通过编写代码解决。编码对于性能也是有影响的,比如你不应该将常量表达式放入循环中,这样会增加计算次数。
软件设计中对性能的影响又包含两个因素,一个是算法和数据结构,一个是程序分解。从技术观点来看,一个程序就是一个算法,不过通常术语“算法和数据结构”指的是查找、排序、访问、压缩以及操作大型的数据集合。算法和数据结构对程序的性能同常有影响,但并不是唯一的因素。程序分解包含了将一个完整的大任务分解成一系列相互关联的子任务、对象体系结构、函数、数据和业务流。
编码对性能的影响也可以分成四个子因素,如上图。
C++对C做了补充和完善,但是有些C++的构件对性能作出了一定的牺牲,这是语言的因素。
当前的不少操作系统设计会让你感觉内存似乎不会用完,可以并行执行,CPU专门为自己的程序服务,统一的内存访问模式,但是实际上即便是采用虚拟内存设计的操作系统,内存也会有用完的时候;CPU不可能总是执行你的程序,而是所有程序轮流使用CPU,每次获得一定的时间片来执行;内存访问模式不是统一的,访问硬盘和内存以及高速缓存都是不一样的,因此性能也不一样;在一个单CPU的机器上,并行执行往往不能带来好处,可能还会导致程序运行速度变慢。
不同的库提供了相同的函数,但是性能表现不一定相同,比如sprintf和itoa。
编译器优化完全取决于编译器实现厂商,因此差异很大。
以性能为名,将设计或代码变得更加复杂,从而导致可读性更差,但是并没有经过验证的性能需求(比如实际的度量数据和与目标的比较结果)作为正当理由,因此本质上对程序并没有好处。
首先,应该关注代码尽可能的清晰易读,清晰的代码更容易优化。先让程序做正确的事情,然后再让它更快速应该较容易。
但是要掌握一些惯用技巧,比如优先使用前缀形式的++,--操作,传递引用,延迟定义变量,使用初始化列表初始化成员变量。
不要过早的使用内联,应该通过工具分析哪些函数真正需要内联。
当真正需要优化性能的时候,首先使用现代性能分析工具找出程序中的主要瓶颈。
但是作为程序库的设计者,预测哪些操作最后会用于性能敏感的客户代码是几乎不可能的。在这种特殊情况下,经验、猜测和对客户代码进行大范围的测试这些手段几乎都要用到。比如:ACE就将自己的wrapper facade层的C++类的成员函数内联,我相信这是经过考验的决定。
如果有继承的情况下,一个派生类构造函数总是先调用父类的构造函数,并且编译器要生成代码以正确的设置虚函数表以及虚函数指针(因为父类通常都需要虚析构函数,所以虚函数表通常是避免不了的),以及父类和子类成员变量的初始化。如果这个继承体系较深的话,付出的代价更多。析构函数因为是构造函数的逆过程,同样也要付出较多的代价。因此在一个性能要求很高的系统中,我们设计C++类需要考虑到这个因素,避免产生多余的父类。(这点很难把握,因为一个符合is a的设计,父类会很自然的被设计出来)
如果在组合情况下,一个类的构造函数必须正确初始化其成员变量,如果成员变量有构造函数,那么也有可能会被调用,析构函数同样如此。
虚函数好处是利用动态类型提供了更好的抽象,客户代码只需要和基类接口打交道,代码更优雅和便于维护。但是虚函数可能会在三个方面影响性能:
1)虚函数表的布局(前面已有论述)的初始化会对构造和析构函数造成性能开销
2)虚函数都是通过运行时查表,然后调用合适的函数,因此比直接的函数调用慢
3)编译器通常对虚函数的内联处理较困难复杂,有些编译器不支持内联虚函数
前面两个不会造成性能的负担,因为如果不使用虚函数,作为一个常用的替代方案,可以给基类定一个一个类型变量,然后每一个子类型在构造函数中设定该变量的值。这些开销和初始化虚函数表指针变量是相等的。然后,还要使用switch/case语句判断类型,并且进行强制类型转换(从基类指针到子类指针),这些开销等价于虚函数表的查找操作。
看来唯一的问题就是如果虚函数比较简单而且被频繁调用,那么不能内联将导致性能的损失。
虚函数带来的性能问题有时候是可以消除的,想想虚函数和继承的密切关系,如果没有继承的话,也就不会出现虚函数。有一种方法可以让我们减少对继承的依赖,模板。
举个例子,如果有一个MyString类,要支持线程安全,我们可能会有mutex和critical section两种线程同步方案,假设存在两个类,它们都实现了lock和unlock函数:
class CriticalSection
{
public:
void lock();
void unlock();
...
};
class Mutex
{
public:
void lock();
void unlock();
...
};
在这里,我们并不打算让这两个类派生自LockBase类,使得lock和unlock函数成为虚函数,它们就是普通的成员函数。我们将MyString设计成模板类:
template<typename lock>
class MyString
{
private:
lock _lock;
};
使用模板技术,我们将运行时多态替换成编译时多态,我们有效的消除了虚函数,因此lock和unlock函数可以被内联的机率大大增加。