C++高效程序设计

Tags: c++

最近开发的一个程序,对代码的速度要求很高,同时由于已实现的代码速度不能满足要求,因此进行了搜索。收藏此篇。

文章来源:http://www.kuqin.com/language/20090314/39898.html


摘要
不管是否愿意承认,每个人都希望程序的运行速度越快越好。每天人们都你追我赶,好像明天就是末日。而同时,公关部的那些家伙则不停的吼叫着,说他们的新引擎比其他人的更“快”更“好”。

我并不打算告诉你如何让你的代码跑得比别人的快。我只是想告诉你,如何让你的代码更快、更高效,当然,是跟你原来的代码相比。
我讲述的内容主要涉及三个概念,这三者之间的关系相当复杂:
1、代码执行时间
2、代码/程序大小
3、程序设计本身的开支
我始终坚信应该保持这三者之间的平衡,尤其在某些情况下,2、3两项直接影响了代码的执行时间。

在本文中,我将讲述一些可能有助于你提高代码执行效率的方法。我会从最简单的优化方法开始,然后逐渐深入到那些比较复杂的技术。现在我们首先从一个不太显眼的地方开始:编译器。

考虑到读者中有一些经验丰富的程序员,我的叙述会尽可能简单,以避免因为细节太多而显得杂乱不堪。

第一节 公欲善其事,必先利其器
这一节的内容似乎不说也罢,不过仔细想想,你对你手中的编译器到底了解多少?你知道它可以为哪些处理器生成代码吗?你知道它可以进行哪些类型的优化吗?你知道它的语言不兼容性吗?
当你想要写出点什么的时候,尤其是当你希望你的代码运行如飞的时候,了解这些内容将是至关重要的。
举例来说,最近在GameDev的讨论组里有人问关于Microsoft Visual C++的“Release Mode”的问题。这是一个标准编译器选项,如果你使用特定的编译器,你就应该知道它的意思。如果你不知道,那很遗憾,你并不真正会使用你花费了大量的金钱买来的东西。简单来说,“Release Mode”会删除所有debug用的代码,进行所有可能的编译代码优化,生成更小的可执行文件,还让这个文件运行的更快。它可能还会有一些其它的功能,如果你感兴趣,请阅读编译器的相关文档。

看到了吧,如果你以前并不知道这个“Release Mode”,我现在就可以告诉你一个让你的代码运行更快的方法,而且这个方法不需要你修改任何代码!

目标平台也是非常重要的。现在,你遇到的最低档的可能就是Intel Pentium处理器了,不过如果你使用10年前的编译器,那么它不会做任何针对Pentium的优化。去找一个最新的编译器,它可能会大大提高程序的运行速度,同样,也不需要你对代码做任何的修改。

另外还要注意一些事:你的编译器有没有代码分析(profiling)工具?如果你连这个都不知道,那么你就不要指望编写出更快的代码了。如果你还不知道什么是代码分析工具,那么你还需要更多的学习。一个代码分析工具就是一个用来获得程序的运行时间的东东。你在代码分析器(profiler)中运行你的程序,做一些操作,然后再从你的程序中退出,就可以获得一个关于每个函数耗时的报告。你可以根据这个报告找到代码的运行瓶颈——就是你的代码中花费时间最多的部分。对这些部分作一些特定的优化比随随便便的在每个地方都做一点优化效果要好多了。

不要说“但是我知道我的瓶颈在哪!”它们可不是光用脑子就可以找到的,尤其是在使用第三方API和程序库时。几个星期前我还遇到一个类似的问题,在一个视频程序里,显示每一帧时都会莫名其妙的产生状态切换,而这个动作占用了总执行时间的25%。通过简单的添加一条测试语句(测试状态是否已经被设置),我把相应的那个函数从分析得到的50个最昂贵的函数列表中剔除了。

看上去在大多数情况下,使用分析器可以很容易达到目的,但事实上并非如此。你必须找到程序中的关键路径。所谓关键路径就是程序大部分运行时间都在执行的路径。对关键路径进行优化可以显著的提高运行效率,你的用户也会因此而高兴。

另一种情况是,也许你发现在某个函数中,时间开支最大的步骤是装载一个特定的文件,但是你知道这种情况只会在应用程序启动时发生一次。对这个函数进行优化也许可以让程序的总运行时间减少几秒钟,但不会提升正常使用时的效率。事实上,这表明你没有进行足够的代码分析,因为在正常使用时,这个函数所占用的时间百分比将会越来越低,而你的关键路径所占用的时间百分比将会一路飙升。

我想以上这些内容能够使你对这些工具有了一些了解。

代码分析工具实在是太好了,记得一定要用!

如果你还没有代码分析器,你可以试试Intel的VTune profiler。你可以免费试用它一个月。在下面这个网址下载http://developer.intel.com/vtune/analyzer/

在本文的下一部分,我将告诉你如何让你的C/C++编译器做你想让它做的事。

第二节 Inlining,inline关键字

什么是inlining?我会通过描述inline关键字来回答这个问题。

Inline关键字告诉编译器“在适当的地方展开函数”,它工作起来很像是C和C++中的宏(#define),但是有一点不同。Inline函数是类型安全的,其主要作用是帮助编译器进行代码优化。有了它,你就可以同时具有宏的速度(没有函数调用的额外开销)和函数的类型安全性,以及一大堆其它好处。

还有什么好处呢?大多数编译器在同一时间内只能优化一个模块中的代码。通常就是一个.h/.cpp文件对。使用inline函数,就使得编译器对在不同的模块中的函数也可以进行代码优化,比如消除返回值拷贝,消除多余的临时变量,等等。如果你想要了解更多关于编译器优化的内容,请参考本文结尾处给出的参考文献,尤其是那本讲述C++高效编程的书。

可怕的inline关键字。我不得不这样说,因为关于它的误解实在太多了。Inline关键字并不强迫编译器inline特定的函数,而只是建议编译器这样做。以下内容引自MSDN:

“The inline keyword tells the compiler that inline expansion is preferred. However, the compiler can create a separate instance of the function (instantiate) and create standard calling linkages instead of inserting the code inline.”(inline关键字告诉编译器最好进行inline扩展。但是,编译器可能会创建一个独立的函数实例和一个标准的调用连接,而不是将代码内联的插入。)

某些情况下编译器会忽略你的inline请求,这些情况包括:在inline函数中使用了循环;在inline函数中调用其它inline函数;递归。

上面引用的那段话还隐含着其它一些内容:一个声明为inline的函数,必须进行内部连接。这就是说,如果你的inline函数在另一个object文件中实现,你的连接器在连接这个函数时就会卡壳。ANSI标准倒是提供了一种方法解决这个问题,可惜的是目前为止Visual C++(6.0)尚不支持这种解决办法。

“那么,”你要问了,“到底应该怎么办呢?”答案很简单:总是在同一个模块中实现inline函数。这个方法做起来很简单,只要将整个函数实现写到.h文件中,并且在所有用到这个函数的模块中包含这个.h文件。也许这并不想你想象中的那么美好,不过它的确可以正常工作。

事实上,考虑到隐藏实现的问题(我是个面向对象偏执狂),我并不喜欢这个方法。但是最近我的确使用这个方法编写了很多类。有一个好处是,我不需要输入inline这个关键字——如果你把整个函数定义放进类定义中,编译器会自动的把它看成inline函数。如果一个类的所有函数都应该是inline的,那么我就把整个类定义及实现都写进头文件中。我建议你只在真正迫切的需要提高运行速度时才这样做,当然,你也不在意太多的人share你的代码。


第三节 搭乘类高速列车
设计执行速度快的类是C++程序设计的关键。我用一个3d向量类来说明这个问题(这在我的工作中是很常见的类)。事实上,就在前几个星期,我刚刚完成了一个向量类。在编写这个类的一个月里,我犯下了太多错误。

一个向量类是必须的,因为工作中有大量的向量数学运算,显然每次都要反复书写相同的内容。如果你想提高编码效率,同时又不想牺牲代码运行速度,那么就要编写一个向量类,我的这一个叫作CVector3f(3f的意思是三个float数据)。为了提高代码的可读性和可维护性,我希望利用C++伟大的特性之一——运算符重载(operator overloading)实现一些运算符函数(+,-,*)。

在最初的设计中,我很快的实现了一个构造函数、一个拷贝构造函数、一个析构函数以及上面提到的那三个运算符。设计过程中,我没有特别考虑效率的问题,也没有使用inline函数,只是简单的把函数声明放入头文件,把函数实现放入.cpp文件中。

下一步是让它跑得更快。我做的第一件事是在头文件中将所有成员函数声明为inline函数。如果编译器真的将它们处理成inline函数,那么我们就可以节省下函数调用的额外开销。对于我的向量类中的那些小函数来说,执行速度有了显著的提升,不过对于那些较大的函数来说,这样做可能不会有明显的效果。

我想到的第二件事是:我们真的需要析构函数吗?正常情况下编译器会为我们生成一个空的析构函数,通常它会比我们写的析构函数效率更高。在我们的向量类中,并没有什么东西需要析构,那么为什么还要浪费时间?

运算符也可以跑得更快。先前的运算符函数大致如下:

CVector3f operator+( CVector3f v )
{
CVector3f returnVector;
returnVector.m_x = m_x + v.m_x;
returnVector.m_y = m_y + v.m_y;
returnVector.m_z = m_z + v.m_z;

return returnVector;
}

这段代码隐藏着众多的多余代码,着实令人烦恼。我们来仔细看看这段代码,代码的第一行声明并构造了一个临时变量。这就是说,这个对象的默认构造函数被调

本文链接:http://www.4byte.cn/learning/67115.html