跟踪实例
我们所使用的每个软件产品都会包括各种跟踪功能,当代码超过几千行时,跟踪功能就显得很重要了。许多C++程序员对此的做法通常是定义一个简单的Trace类将诊断信息打印到日志文件中。程序员可以再每个想要跟踪的函数中定义一个Trace对象,在函数的入口和出口Trace类可以分别写一条信息。尽管这种做法可能会给程序增加额为的执行开销,但是他能帮助程序员找出问题而无需使用调试器。
最理想的跟踪性能优化的方法应该能够完全消除性能开销,即把跟踪调用嵌入在#ifdef块内
#ifdef TRACE
Trace t("myFunction"); //构造函数以一个函数名作为参数
t.debug("Some information message");
#endif
使用这种方法的不足在于必须重新编译程序来打开或关闭跟踪。还有一种选择:你可以通过与正在运行的程序通信来动态地控制跟踪,Trace类能够在记录任何跟踪信息之前先检查跟踪状态:
void Trace::debug(string &msg)
{
if (traceIsActive)
{
//在此记录消息
}
}
在跟踪处于活动状态时我们并不关心性能。通常情况下其处于非活动状态,我们希望自己的代码此时表现出最好的性能。下面是一条典型的跟踪语句:
t.debug("x = " + itoa(x)); //函数itoa()把一个int值转换为ASCII
这条语句隐含了大量的计算:
①为"x = "创建一个临时string对象
②调用itoa(x)
③从itoa()返回的字符串指针创建一个临时的string对象
④链接上述string对象而创建第三个临时的string对象
⑤在debug()调用返回后销毁全部三个string临时对象
初步跟踪的实现
int myFunction(int x)
{
string name = "myFunction";
Trace t(name);
...
string moreInfo = "more interesting info";
t.debug(moreInfo);
...
}
下面用Trace类实现上面功能:
class Trace
{
public:
Trace(const string &name);
~Trace();
void debug(const string &msg);
static bool traceIsActive;
private:
string theFunctionName;
};
inline Trace::Trace(const string &name) : theFunctionName(name)
{
if (traceIsActive)
{
cout << "Enter function " << name << endl;
}
}
inline void Trace::debug(const string &msg)
{
if (traceIsActive)
{
cout << msg << endl;
}
}
inline Trace::~Trace()
{
if (traceIsActive)
{
cout << "Exit function " << theFunctionName << endl;
}
}
一旦Trace类被设计、编码和测试好后,他就被部署到大部分代码中。在接下来的性能测试中我们会震惊的发现性能陡然降为原来性能的20%。C++的程序员们可能对性能有不同的理解,但是有几个基本原则:
①I/O的开销是昂贵的
②函数调用的开销是要考虑的一个因素,因此我们应该将短小的、频繁使用的函数内联
③复制对象的开销是昂贵的,最好选择引用传递而不是值传递
很显然,之前的Trace类遵循了以上三个原则,但却未能如愿。所以上述三个原则所体现的智慧没有击中开发高性能C++所要求的专门技术要害。Trace实现就是一个无用对象对性能带来破坏性影响的实例,即便是对其简单的调用,这一点能明显体现出来。对Trace对象最低限度的使用就是把进入函数和离开函数记录到日志中:
int myFunction(int x)
{
string name = "muFunction";
Trace t(name);
...
}
这种最低限度的跟踪引发了一系列计算:
①创建一个作用于为myFunction的string型变量name
②调用Trace的构造函数
③Trace的构造函数调用string的构造函数来创建一个成员string
④销毁string型变量name
⑤调用Trace的析构函数
⑥Trace的析构函数为成员string调用string的析构函数
下面考虑Trace的代价究竟有多大。为了找出基准度量,我们对函数AddOne()的百万次迭代执行进行了计时:
int AddOne(int x) //版本0
{
return x + 1;
}
int main()
{
Trace::traceIsActive = false; //关闭跟踪
//...
GetSystemTime(&t1); //开始计时
for (i = 0; i < j; i++)
{
y = AddOne(i);
}
GetSystemTime(&t2); //结束计时
//...
}
下一步,为了评估性能变化,我们在AddOne中添加一个Trace对象并重新测量
int AddOne(int x) //版本1
{
string name = "AddOne";
Trace t(name);
return x + 1;
}
接下来我们从AddOne创建的并传递给Trace构造函数的string参数入手。把string对象改成简单的char型指针:
int AddOne(int x) //版本2
{
char *name = "AddOne";
Trace t(name);
return x + 1;
}
相应的构造函数也需要改变
inline Trace::Trace(const char *name) : theFunctionName(name)
if (traceIsActive)
{
cout << "Enter function " << name << endl;
}
}
通过上述三个版本的AddOne函数的迭代,我们会发现各自的运行时间
版本0:55ms
版本1:3500ms
版本2:2500ms
从性能的观点出发,有两种等价的解决方案。第一种是把string对象替换为一个简单的char型指针,没有什么开销。另一种是使用复合代替聚合,用string型指针而不是把string子对象嵌入到Trace对象里面。string指针与string对象有一个优势:可以把string的创建推迟到确定跟踪处于打开状态以后。所以应该采用这种方法解决问题。
class Trace //版本3
{
public:
Trace (const char *name) : theFunctionName(0)
{
if (traceIsActive)
cout << "Enter function " << name << endl;
theFunctionName = new string(name);
}
...
private:
string *theFunctionName;
};
inline Trace::~Trace()
{
if (traceIsActive)
{
cout << "Exit function " << *theFunctionName << endl;
delete theFunctionName;
}
运行版本3的程序,我们会发现响应时间从2500s下降到185ms。在某些方面,这与内联很相似,内联对大块头函数的影响是无足轻重的。只有在针对那些调用和返回开销占全部开销的绝大部分的小型函数时,内联对性能的改善才有比较大的影响。完美适合内联的函数就恰好不适合跟踪。由此可以得出结论:Trace对象不应被添加到小型、频繁执行的函数中。
总结
1 内联会减少函数调用和返回的开销
2 使用引用传递对象从而避免对象的复制
3 不要把精力浪费在计算结果根本不会被使用的地方
4 在完成同样的简单工作时,char型指针有时可以比string对象更有效
5 内联Trace的构造函数和析构函数使得消除Trace的开销变得更容易