【纸上得来终觉浅】
这里所说的跟踪指的是跟踪程序的运行过程。这里讨论跟踪,旨在如何利用高效的跟踪的代码,使得跟踪尽可能不增加源程序的额外开销。
这里的跟踪其实很直观,就是顺着程序运行的轨迹,怎样打印出程序的每一个阶段的运行状态的log信息。因此,如何跟踪代码便是一个核心的问题。不当的跟踪方式会导致运行开销的增大。因此怎样设计出高效的跟踪技术是我们学习怎样写出高效的C++代码的一个很好的突破口。
鉴于C++的面向对象的特征,最直观的便是定义一个简单的Trace类将程序的状态信息打印到日志文件中,程序员可以在每一个需要跟踪的程序段中使用定义一个Trace类的对象,在函数的入口和出口打印错误的信息。最理想的跟踪就是在跟踪方法中完全消除性能的开销,即把跟踪调用放在 #ifdef的条件编译的块内。比如:
#ifdef
Trace t(“MyFunction”);
t.debug(“some debug information”);
#endif
上述代码使用条件编译来跟踪程序,这样的跟踪技术最大的不足便是必须重新编译来打开和跟踪技术,但是这仅仅是对于免费的开源软件来说的。除非你愿意将你自己的代码开源给用户使用。因此,我们需要开放接口给用户,也就是条件性的选择是否打开跟踪,也就是说先检查跟踪的状态再记录跟踪的信息。
例如:
void Trace::debug(string &word)
{
if(active)
{
//在此记录信息
}
}
这里的程序的意图应该就是说在确定问题之后再打开跟踪的功能,跟踪默认是属于非活动的状态,这样我们就能保证程序在执行的过程中处在最好的性能上,但是事与愿违,例如下面的代码:
t.debug(“t = ” + itoa(x));
这条典型的跟踪语句便会导致严重的性能的问题,即使我们关闭了程序的跟踪功能,这条语句隐含的计算量绝对是不容小觑的:
1 为 “x= ” 建立一个临时的string的对象
2 调用函数 itoa;
3 从itoa返回的字符指针创建一个string的临时对象
4 连接上述的两个string构建第三个临时的string的对象。
5 在debug调用返回时调用这三个临时对象的析构函数。
基于此,要构造一个性能高效的跟踪函数是很有必要的。下面一步一步的进行解释:
对于一个函数的跟踪,大致是下面这样的代码:
int myFunction(int x)
{
string name = “myFunction”;
Trace t(name);
//do something
string meminfo = “more interesting info”;
t.debug(moreinfo);
}
class Trace
{
public:
Trace Trace(const string &);
~Trace();
void debug(const string &name);
static bool active;
private:
string theFunctionName;
};
Trace的构造函数大致是这样:
inline Trace::Trace(const string &name):theFunctionName(name)
{
if(active)
{
cout << name << endl;
}
}
debug则用于打印一些附加的信息:
inline void Trace::debug(const string &info)
{
if(active) cout << info << endl;
}
inline Trace::~Trace()
{
if(active) cout << “exit ” << endl;
}
但是这样就会出现,在程序中的大量的Trace的临时对象的频繁创建与销毁,使得程序的开销急剧增大。
首先我们需要分析一下上面的代码,我们将构造函数和析构函数设置为inline以减少函数的调用的开销,其次我们在传递函数对象的时候为了降低参数传递的开销使用了引用传值,首先,这些代码本身是高效的,但是加入了跟踪之后我们做了很多不该做的操作,导致性能的降低。
下面是一个简单的测试:首先定义一个简单的函数:
void increment(int x)
{
return x + 1;
}
在该函数中加入了Trace的信息之后,该函数大致是:
int increment2(int x)
{
string name = "increment2";
Trace t(name);
t.debug("enter...");
return x+1;
}
完整的代码为:
#include <iostream>
#include <string>
using namespace std;
int increment(int x)
{
return x+1;
}
class Trace
{
public:
inline Trace(const string &_name):name(_name){
if(active)
cout << "enter function " << name << "..." << endl;
}
inline ~Trace()
{
if(active)
cout << "exit..." << endl;
}
inline void debug(const string &msg)
{
if(active)
cout << msg << endl;
}
private:
string name;
static bool active;
};
bool Trace::active = true;
int increment2(int x)
{
string name = "increment2";
Trace t(name);
t.debug("enter...");
return x+1;
}
int main(void)
{
int x = 0;
clock_t start,finish;
double total=0;
start = clock();
for(int i=0;i<1000000;++i)
increment2(x);
finish = clock();
total = static_cast<double>(finish-start)/CLOCKS_PER_SEC;
start = clock();
for(int i=0;i<100000000;++i)
increment(x);
finish = clock();
total = static_cast<double>(finish-start)/CLOCKS_PER_SEC;
cout << "running time = " << total << " s." << endl;
cout << "running time = " << total << " s." << endl;
return 0;
}
我们已经提到上述代码中得额外性能开销来自于string临时对象的创建与撤销,这里我们无论是否打开了Trace的功能,我们总是会有这样的开销,下面的改进可以使整体的性能开销降低:
1 用char指针取代string对象,这样只有在active为true是才创建字符数组用来存放指针。
2 对于一些短小的经常调用的函数不建议使用Trace功能,因为函数本身的执行时间就比较的短,加入的语句对于整体函数的执行的影响是很大得,想象在一个只有两行的函数中添加几行的Trace语句与在一个几百行的程序中添加Trace语句,可能短小的函数的运行时间会double,但是对于几百行的函数的影响便可以忽略不计。
总结:
在任何需要跟踪的程序中,如何写出高效的跟踪代码,使得程序的性能不会受到Trace的影响是很重要的。一定要注意一些隐形的开销,比如临时对象的创建以及撤销,以及函数采用引用传值,短小函数尽量内联,减少函数调用的开销。总而言之,尽量避免Trace的引入导致程序的性能出现数量级的改变。