一般的C ++性能改进技巧
有人能指点我一篇文章,或者在这里写一些关于一般有效的c ++编程习惯的技巧(没有真正的缺点),并提高性能? 我不是指编程模式和algorithm复杂性 – 我需要一些小的东西,比如你如何定义你的函数,要做什么/为了避免循环,在堆栈上分配什么,在堆上是什么,等等。
这不是关于如何快速制作一个特定的软件,也不是关于如何创build一个干净的软件devise,而是编写习惯,如果你总是使用它们,那么你的代码会比慢一点慢一点。
谢谢 :)
有效的C ++ , 更高效的C ++ , 有效的STL和C ++编码标准中的许多技巧都是沿着这条线的。
这种提示的一个简单的例子:尽可能使用preincrement(++ i)而不是postincrement(i ++)。 这对迭代器来说尤其重要,因为后置增量涉及复制迭代器。 你的优化器也许能够解决这个问题,但是为了避免这种风险,写出预增量并不是什么额外的工作。
如果我正确地理解了你的话,你就是要避免过早的悲观化 ,这是避免过早优化的一个很好的补充。 根据我的经验,要避免的第一件事是尽可能不要复制大对象。 这包括:
- 通过(const)引用来传递对象
- 在任何情况下通过(const)引用返回对象
- 确保你需要的时候声明一个引用variables
最后一个项目符号需要一些解释。 我不能告诉你我见过这么多次:
class Foo { const BigObject & bar(); }; // ... somewhere in code ... BigObject obj = foo.bar(); // OOPS! This creates a copy!
正确的方法是:
const BigOject &obj = foo.bar(); // does not create a copy
这些准则适用于大于智能指针或内置types的任何内容。 此外,我强烈build议您投入时间学习来分析您的代码 。 一个好的分析工具将有助于避免浪费的操作。
我的一些宠物小偷:
- 在使用/初始化之前不要声明(实际上是定义)对象variables(如C)。 这就要求构造函数和赋值运算符函数能够运行,而且对于复杂的对象来说可能是昂贵的。
- 优先预增量后增量。 这只对重载操作符和用户定义的types有重载操作符。
- 尽可能使用最小的基本types。 不要使用long int来存储范围为0..5的值。 这将减less整体内存使用率,改善局部性,从而提高整体性能。
- 仅在必要时使用堆内存(dynamic分配)。 许多C ++程序员默认使用这个堆。 dynamic分配和释放是昂贵的。
- 最大限度地减less使用临时(特别是基于string的处理)。 Stroustrup提供了一种在“C ++编程语言”中定义三元和更高阶算术运算符的逻辑等价物的好技术。
- 了解你的编译器/链接器选项。 也知道哪些会导致非标准的行为。 这些显着影响运行时性能。
- 了解STL容器的性能/function折衷(例如,不要频繁地插入向量,使用列表)。
- 当他们将被无条件分配时,不要初始化variables,对象,容器等。
- 考虑复合条件的评估顺序。 例如,
if (a && b)
给定,如果b更可能是错误的,则首先将其保存为a的评估。
还有很多其他的“坏习惯”,我不会提到,因为在实践中,现代编译器/优化器将消除不良影响(例如,返回值优化与参考传递,循环展开等)。
其中一个很好的出发点是本周系列的Sutter's Guru ,以及由此产生的Exceptional C ++书籍。
使用函子( operator()
实现类),而不是函数指针。 编译器有一个更简单的工作内联前者。 这就是为什么C ++的std::sort
往往比C的qsort
更好(当给定一个函qsort
。
Agner Fog的 “ C ++优化软件 ”通常是优化技术的最佳参考之一,既简单又绝对更先进。 另外一个很大的好处是可以在他的网站上阅读。 (请参阅他的网站链接,并链接在PDF文件标题)。
编辑:还记得90%(或更多)的时间花在10%(或更less)的代码中。 所以通常情况下,优化代码实际上就是查明瓶颈。 进一步讲,知道现代编译器比大多数编码器做得更好,特别是微观优化,比如推迟variables的初始化,等等,编译器往往非常擅长优化,所以花时间编写稳定可靠的代码和简单的代码。
我认为,至less在大多数情况下,更多地关注algorithm的select而不是微观优化。
从你的问题来看,你已经知道“过早优化是邪恶”的哲学,所以我不会那样讲。 🙂
现代编译器已经非常聪明,为你微观优化事物。 如果你太努力了,你可能经常比原来的直接代码慢。
对于小的“优化”,您可以安全地不做任何思考,也不会影响代码的可读性/可维护性,请查阅Sutter&Alexandrescu所着的“ C ++ Coding Standards ”一书中的“过早的悲观化”部分。
有关更多优化技术,请参阅Bulka&Mayhew的Efficient C ++ 。 只有通过分析才能使用!
对于一般的C ++编程实践,请查看:
- Sutter&Alexandrescu的C ++编码标准 (必须有,恕我直言)
- Scott Meyers的有效C ++ / STL系列
- Herb Sutter 特别的C ++系列
在我的头顶,一个很好的一般性能练习是通过引用传递重量级对象,而不是通过复制。 例如:
// Not a good idea, a whole other temporary copy of the (potentially big) vector will be created. int sum(std::vector<int> v) { // sum all values of v return sum; } // Better, vector is passed by constant reference int sum(const std::vector<int>& v) { // v is immutable ("read-only") in this context // sum all values of v. return sum; }
对于像复数或二维(x,y)点这样的小对象,该函数可能会以复制传递的对象运行得更快。
当涉及到固定大小,中等重量的对象时,function运行速度会随着对象的复制或引用而变得不太明显。 只有分析才能说明。 我通常只是通过const引用(如果函数不需要本地副本),只有在分析告诉我时才担心它。
有人会说,你可以毫不费力地插入小class的方法。 这可能会给你提供一个运行时性能提升,但是如果有大量的内联,它也可能延长你的编译时间。 如果类方法是库API的一部分,最好不要内联,不pipe它多小。 这是因为内联函数的实现必须对其他模块/类可见。 如果你改变了内联函数/方法中的某些东西,那么引用它的其他模块需要重新编译。
当我第一次开始编程的时候,我会试着微观地优化一切(那是我的电气工程师)。 多浪费时间!
如果你进入embedded式系统,事情就会改变,你不能把内存看成是理所当然的。 但那是另一种蠕虫。
这是一个关于这个主题的好文章: 如何慢下来
使用正确的容器
序列容器
- 如果要继续向其添加数据,请不要将
vector
用于未知大小的数据。 如果要重复调用push_back()
,请使用reserve()
或者使用deque
。 - 如果您打算在容器中间添加/删除数据,则
list
可能是正确的select。 - 如果您要添加/删除容器两端的数据,
deque
可能是正确的select。 - 如果您需要访问容器的第n个元素,
list
可能是错误的select。 - 如果您需要访问容器的第n个元素并在中间添加/移除元素,则对所有三个容器进行基准testing。
- 如果你有C ++ 0x能力并且正在使用一个
list
但是你永远不会在列表中向后移动,你可能会发现更多符合你的喜好的forward_list
。 它不会更快,但会占用更less的空间。
请注意,这个build议变得更适用于容器越大。 对于较小的容器, vector
可能总是正确的select,只是因为较低的常数。 如有疑问,基准。
关联容器
- 如果您没有TR1,C ++ 0x或供应商特定的
unordered_foo
/hash_foo
,则没有hash_foo
select。 使用四个容器中的任何一个都适合您的需求。 - 如果你有一个
unordered_foo
,如果你不关心元素的顺序,而且你有一个很好的types的散列函数,可以使用它来代替有序的版本。
明智地使用例外
- 不要在正常的代码path中使用exception。 当你真的有特殊情况时,保存它们。
爱模板
- 模板会在编译时和空间上花费你的成本,但是如果你在运行时进行了计算,那么性能的提升会是惊人的。 有时候甚至是如此低调的东西。
避免dynamic_cast
-
dynamic_cast
有时是做某事的唯一select,但通常可以通过改进devise来消除dynamic_cast
的使用。 - 不要用
typeid
和static_cast
replacedynamic_cast
。
模板! 使用模板可以减less代码的数量,因为您可以拥有可以使用许多数据types重用的类或函数/方法。
考虑以下:
#include <string> using std::basic_string; template <class T> void CreateString(basic_string<T> s) { //... }
basic_string可以由char,wchar_t,unsigned char或unsigned wchar_t组成。
模板也可以用于各种不同的东西,如特征,类专业化甚至用于传递int值给一个类!
除非你真的确定另一个容器types更好,否则使用'std :: vector'。 即使“std :: deque”,“std :: list”,“std :: map”等似乎更具有方便性的select,一个向量在内存使用和元素访问\迭代次数方面都优于它们。
此外,更喜欢使用容器成员algorithm(即'map.equal_range(…)'),而不是他们的全球对手('std :: equal_range(begin(),end()…)')
我喜欢这个问题,因为它需要一些“好习惯”。 我发现编程中某些令人满意的东西最初是一件苦差事,但一旦变成习惯就变得可以接受,甚至容易。
一个例子是总是使用智能指针而不是原始指针来控制堆内存的生存期。 另一个与之相关的问题就是养成了一直使用RAII进行资源获取和发布的习惯。 另一个总是使用exception进行error handling。 这三种方法往往简化代码,从而使代码变得更小,速度更快,而且更容易理解。
你也可以让getter和setter隐式内联; 总是充分利用构造函数中的初始化列表; 并始终使用std库中提供的查找和其他相关函数,而不是制作自己的循环。
不是特别的C ++,但是避免数据复制通常是值得的。 在有很多内存分配的长时间运行的程序中,将内存分配作为devise的主要部分是值得的,所以你使用的内存来自被重用的内存池,尽pipe这对于被认为是值得养成习惯的。
还有一件事 – 如果你需要function,不要将代码从一个地方复制到另一个地方 – 使用一个函数。 这样可以保持代码的小型化,并使得使用这个function的所有地方都更容易优化。
避免尽可能多次迭代相同的数据集。
这是我以前提到的列表 – http://www.devx.com/cplus/Article/16328/0/page/1 。 除此之外,谷歌search的C + +性能技巧产量相当多。
我build议阅读Jon Bentley的“编程珍珠”第二章(“性能”)。 这不是C ++特有的,但是这些技术也可以应用在C或C ++中。 该网站只包含书中的部分内容,我build议阅读本书。
我习惯于习惯于编写++i
而不是i++
而不是当i
是一个int
时它会带来任何性能提升,但是当i
是一个可能有复杂实现的iterator
,情况会有所不同。
那么让我们假设你来自C编程语言,放弃了在函数开头声明所有variables的习惯:在函数stream中需要时声明variables,因为函数可能包含早期return
语句,初始化时被有效地使用。
除此之外,另一个资源是Herb Sutter(又是他)和Alexei Alexandrescu的“C ++编码标准:101条规则,指南和最佳实践” 。
还有Scott Meyers的Effective C ++: Effective C ++的更新版本:55个改进程序和devise的具体方法 。
最后,我想提一提托尼 ·阿尔布雷希特的“面向对象编程的陷阱”的介绍:并不是说它包含了盲目遵循的经验法则,而是一个非常有趣的读法。
本页总结了关于C ++优化(无论是在编写软件的同时还是之后)所需要了解的一切。 这是一个非常好的build议,非常有用,可以在项目的优化阶段作为有用的提示。
这有点旧,所以你也必须知道你的编译器已经完成了许多优化(比如NRVO)。
除此之外,阅读已经被引用的有效的C ++,更有效的C ++,有效的STL和C ++编码标准也很重要,因为它解释了关于语言和STL中发生的事情的许多事情,使您能够更好地通过更好地了解到底发生了什么,优化您的具体情况。
这里有很多好的build议。
养成良好习惯的最好方法之一是强迫自己。 为此我爱PC-Lint。 PC-Lint将实际执行Scott Meyer的Effective C ++和更有效的C ++规则。 同时服从Lint规则往往会导致维护更容易,错误更less,代码更简洁。 当你意识到lint通常会产生比源代码更多的输出时,请不要太疯狂; 我曾经做过一个150MB的源代码和1.8GB的Lint消息。
- 避免内存碎片。
- alignment的内存。
- SIMD指令。
- 无锁multithreading。
- 使用适当的加速树,如kd树,覆盖树,八叉树,四叉树等。 以前三种方式来定义这些(即,使节点全部在一个块中)
- 内联。 最低的悬挂,但相当美味的水果。
性能提升你可以得到这种方式是惊人的。 对于我来说1500倍的计算量大的应用程序。 不要过于粗暴,而要用类似的数据结构写在一个主要的软件包中。
我不打扰像post precrecrement后贴。 这只会给某些(不重要的)情况下的储蓄,而且大部分提到的东西都是类似的东西,可能会在一段时间内多出1%,但通常是不值得花费的。
倾向于使用预增量。
与int /指针等,它没有任何区别。
但是对于类types,标准的实现方式需要创build一个新的对象。
所以更喜欢前增量。 以防万一以后,types也会改变。
那么你将不需要修改代码来应付。
提高这些技能的最好方法是阅读书籍和文章,但我可以帮助你一些提示:
- 1-按引用和基本types或指针types接受对象,但如果函数存储引用或指向对象的指针,则使用对象指针。
- 2-不要使用MACROS来声明常量 – >使用静态常量。
- 3-如果您的课程可能被分类,请始终实施虚拟析构函数。
- 避免多重inheritance。
- 在必要时使用虚拟,而不只是为了好玩
- 只有在不痛苦的情况下才能使用模板化的集合类
为什么没有人提到它呢? 为什么每个人都变得可怜++i
呢?
你可以轻松做的最好的小事之一, 不要悲观的代码:
Scott Meyers的 有效C ++ ,第20项:
优先传递引用到const传递值
例:
// this is a better option void some_function(const std::string &str); // than this: void some_function(std::string str);
在短std::string
情况下,你可能赢不了多less,但是通过这样的大对象可以为你节省相当多的计算能力,因为你避免了多余的复制。 而且如果你忘记实现你的拷贝构造函数,也可以从一个或两个bug中拯救你。
这一个将有非常好的技术一般的C + +优化:
http://www.tantalon.com/pete/cppopt/main.htm
使用Profiler查看应用程序的哪个部分运行缓慢,然后使用优化技术。
我用valgrind和callgrind工具来进行性能分析,它会给你多less线路。
valgrind –tool = callgrind