一般C++性能改进技巧

gen*_*sys 49 c++

有人能指点我一篇文章,或者在这里写一些关于一些C++编程习惯的技巧,这些习惯通常是有效的(没有真正的缺点)并且可以提高性能吗?我不是指编程模式和算法的复杂性 - 我需要一些小的东西,比如你如何定义你的函数,要做的事情/要避免在循环中,在堆栈上分配什么,堆上的内容等等.

这不是关于如何更快地制作一个特定的软件,也不是关于如何创建一个干净的软件设计,而是关于编程习惯 - 如果你总是应用它们,你将使你的代码比速度稍慢一点.

Lau*_*ves 33

有效的C++,更有效的C++,有效的STLC++编码标准中的许多技巧都在这一行.

这样一个提示的一个简单例子:尽可能使用preincrement(++ i)而不是postincrement(i ++).这对于迭代器尤为重要,因为后增量涉及复制迭代器.你的优化器可能能够解除这个,但是编写preincrement并不是额外的工作,那么为什么要冒这个风险呢?

  • 我听过一些人提到编写效率低下的代码的做法,这些代码并不像"过早的悲观化"那样容易读/写. (14认同)
  • @BillyONeal,尝试以这样的方式读取像'++ i`这样的预增量:"增量i",然后将它与`i ++`:"i increment"进行比较.哪个听起来更合适?希望现在你更少讨厌这种增量形式.:) (9认同)
  • 编译器可以优化`i ++`对于原语来说和`++ i`一样高效,但对于迭代器则不然,并且一致性很好. (8认同)
  • 我使用它不是因为它更快,而是因为我发现它更具表现力."增量i"写成"++ i".后增量也可能导致逻辑错误,因为预先增量 - 问题是如果你使用错误的,你会得到错误的答案. (4认同)
  • 我个人讨厌看到++ i.对我来说,我喜欢看到操作员左侧修改的对象修改它.在现代编译器上,它无论如何都没有区别. (2认同)
  • @大卫·索恩利:同意。如果 90% 或更多的 C/C++ 程序员甚至不知道“序列点”的概念,我不会感到惊讶,这表明在表达式中如此普遍地产生副作用是一个坏主意。我记得当我阅读 comp.lang.c 时(很久以前),最常见的问题之一是“i = i++ 是什么意思?”。我发誓每周至少会问一次类似的问题。(答案是它是未定义的,因为 i 被突变两次而没有插入序列点。) (2认同)

Mic*_*fik 26

如果我理解正确的话,那就是要求避免过早的悲观化,这是避免过早优化的一个很好的补充.根据我的经验,要避免的首要任务是尽可能不复制大型对象.这包括:

  • 通过(const)引用函数传递对象
  • 只要实际,通过(const)引用返回对象
  • 确保在需要时声明引用变量

最后一个子弹需要一些解释.我不能告诉你我见过多少次:

class Foo
{
    const BigObject & bar();
};

// ... somewhere in code ...
BigObject obj = foo.bar();  // OOPS!  This creates a copy!
Run Code Online (Sandbox Code Playgroud)

正确的方法是:

const BigOject &obj = foo.bar();  // does not create a copy
Run Code Online (Sandbox Code Playgroud)

这些指南适用于大于智能指针或内置类型的任何内容.此外,我强烈建议花时间学习分析您的代码.一个好的分析工具将有助于捕获浪费的操作.

  • 你听说过复制吗? (4认同)
  • 我会阅读(http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/)并思考为什么以及何时你可能想要使用复制语义而不是盲目处方`const& `无处不在. (3认同)
  • @Inverse:我听说过它,但我不希望它发生在编译单元之间。 (2认同)
  • @Mike:RVO不需要内联(毕竟,它在语义上等同于从调用者传递指向目标位置的指针,并在被调用者中执行新的放置),因此不会受到任何影响.单位边界. (2认同)

小智 16

A few of my pet peeves:

  1. Don't declare (actually, define) object variables before their use/initialization (as in C). This necessitates that the constructor AND assigment operator functions will run, and for complex objects, could be costly.
  2. Prefer pre-increment to post-increment. This will only matter for iterators and user-defined types with overloaded operators.
  3. Use the smallest primitive types possible. Don't use a long int to store a value in the range 0..5. This will reduce overall memory usage, improving locality and thus overall performance.
  4. 仅在必要时使用堆内存(动态分配).许多C++程序员默认使用堆.动态分配和解除分配是昂贵的.
  5. 尽量减少临时使用(特别是基于字符串的处理).Stroustrup提供了一种很好的技术,用于在"C++编程语言"中定义三元和更高阶算术运算符的逻辑等价.
  6. 了解您的编译器/链接器选项.还要知道哪些导致非标准行为.这些会显着影响运行时性能
  7. 了解STL容器的性能/功能权衡(例如,不要频繁插入向量,使用列表).
  8. 当无条件地分配变量,对象,容器等时,不要初始化它们.
  9. 考虑复合条件的评估顺序.例如,给定if (a && b),如果b更可能是假的,则首先将其放置以保存a的评估.

还有许多其他"坏习惯",我不会提及,因为在实践中,现代编译器/优化器将消除不良影响(例如,返回值优化与传递引用,循环展开等).

  • 我在软件维护方面不同意#8.仅仅是因为最初无条件地分配了某些东西并不意味着它总是会.对于绝大多数情况,初始化变量是一个非常好的主意. (10认同)
  • 我不同意#3.如果处理器的本机字是32位,则它被优化为处理32位; 是否使用全部或更少.某些处理器在尝试访问较小尺寸的变量时可能会窒息或堵塞.例如,8 |中的ARM处理器 32位模式(8位字符,32位整数)具有16位数量的硬盘时间.因此,与int(32位)相比,short int(16位)实际上可能会降低处理器的速度.使用整数,只在必要时使用短整数. (10认同)
  • 我无法与 ARM 处理器交谈,但在 x86 上,如果您的变量跨越内存中的单词边界,您只会遇到这样的问题。当您使用不是字大小的变量时,这种情况更有可能发生。当然,大多数编译器会将内存中的变量与字大小对齐,从而避免这个问题(并且可能使用与最初使用字大小的变量相同的内存量)。 (2认同)

Nik*_*sov 15

其中一个好的起点是Sutter的本周系列大师,以及由此产生的Exceptional C++书籍.


Tom*_*sen 12

Agner Fog的" 在C++中优化软件 " 通常是优化技术的最佳参考之一,既简单又明显,也更先进.另一个很大的优点是可以在他的网站上免费阅读.(请参阅他的网站名称中的链接,以及pdf的纸质标题链接).

编辑:还要记住,90%(或更多)的时间花费在代码的10%(或更少)上.因此,一般来说优化代码实际上是针对您的瓶颈.进一步就知道,现代的编译器会做optimzation比大多数程序员要好得多,特别是微优化,如延缓变量初始化等编译器往往在优化非常好,所以花时间写稳定,可靠是重要和有益的和简单的代码.

我认为,至少在大多数情况下,更多地关注算法的选择而不是微优化是值得的.

  • 你应该追求正确和高绩效.人们总是将两者相互对立,好像你不能同时拥有两者,但你可以而且应该. (7认同)
  • 根据我的经验,应该在程序的正确性和稳健性方面花费更多精力而不是性能.与较慢的程序很少锁定或崩溃相比,经常锁定或崩溃的高性能程序没什么价值. (2认同)
  • @Dan我完全同意你的看法.但是我已经看到许多代码,善意的人们试图优化代码,而唯一的结果就是让编译器更难以优化代码.我认为,通过仔细选择您的设计和算法,您可以真正提升程序性能的地方.当然,需要一个分析器来识别那些占据90%的10%代码,并找出它是否可以进一步优化(以及如何). (2认同)

小智 11

使用仿函数(已operator()实现的类)而不是函数指针.编译器可以更轻松地内联前者.这就是为什么C++ std::sort比C更倾向于表现得更好(当给出一个仿函数时)qsort.

  • `std :: sort`在很大程度上也做得更好,因为它也可以避免需要额外的间接(将两个对象的地址作为`void*`进行比较,而不是对象本身,即使普通副本更便宜) . (2认同)

Emi*_*ier 8

从你的问题看来,你已经知道"过早优化是邪恶的"哲学,所以我不会那么鼓吹.:)

现代编译器已经非常聪明地为您进行微优化.如果你太努力,你通常可以比原始的直接代码慢.

对于小的"优化",您可以安全地进行操作,而不会影响代码的可读性/可维护性,请查看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;
}
Run Code Online (Sandbox Code Playgroud)

对于像复数或二维(x,y)点这样的小对象,对于通过复制传递的对象,该函数可能会运行得更快.

对于固定大小的中等重量对象,如果函数通过副本或对象的引用运行得更快,则不太清楚.只有剖析才能说明问题.我通常只是通过const引用传递(如果函数不需要本地副本),只有在分析告诉我时才会担心它.

有些人会说你可以不假思索地内联小类方法.这可能会提高运行时性能,但如果有大量内联,它也可能会延长编译时间.如果类方法是库API的一部分,那么最好不要内联它,无论它有多小.这是因为内联函数的实现必须对其他模块/类可见.如果您在内联函数/方法中更改了某些内容,则需要重新编译引用它的其他模块.

当我第一次开始编程时,我会尝试微观优化一切(那是我的电气工程师).真是浪费时间!

如果您使用的是嵌入式系统,那么事情会发生变化,您无法将记忆视为理所当然.但这是另一整套蠕虫.

  • @rib:Ahem ...... Emile是一个法国**男性化的**.最后一个'e'是沉默的.不要与Emilie混淆.:-) (3认同)

Nem*_*vic 5

这是一篇关于这个主题的好文章:如何变慢


Vik*_*ehr 5

除非您真的确定另一种容器类型更好,否则请使用“std::vector”。即使“std::deque”、“std::list”、“std::map”等看起来更方便,向量在内存使用和元素访问\迭代次数方面都优于它们。

此外,更喜欢使用容器成员算法(即'map.equal_range(...)')而不是其全局对应算法('std::equal_range(begin(), end()...)')


Per*_*uid 5

我喜欢这个问题,因为它要求一些"好习惯".我发现编程中可取的某些东西最初是一件苦差事,但一旦成为习惯就变得可以接受甚至变得容易.

一个示例始终使用智能指针而不是原始指针来控制堆内存生存期.当然,另一个相关的问题是养成了一直使用RAII进行资源获取和发布的习惯.另一个是始终使用异常进行错误处理.这三种方法倾向于简化代码,从而使代码更小,更快,更容易理解.

你也可以隐式内联getter和setter; 总是充分利用构造函数中的初始化列表; 并始终使用std库中提供的find和其他相关函数,而不是制作自己的循环.

不是特别是C++,但通常值得避免数据复制.在具有大量内存分配的长时间运行的程序中,将内存分配视为设计的主要部分是值得的,因此您使用的内存来自可重用的池,尽管这不一定是常见的事情.被认为值得养成习惯.

还有一件事 - 如果您需要功能,请不要将代码从一个地方复制到另一个地方 - 使用一个功能.这样可以减小代码大小,并且可以更轻松地优化使用此功能的所有位置.


cop*_*pro 5

使用正确的容器

序列容器

  • 千万不能使用vector,如果你要继续添加数据为未知大小的数据.如果要重复呼叫push_back(),请使用reserve()deque改用.
  • 如果您要在容器中间添加/删除数据,list则可能是正确的选择.
  • 如果您要从容器的两端添加/删除数据,deque则可能是正确的选择.
  • 如果需要访问容器的第n个元素,list可能是错误的选择.
  • 如果您需要同时访问容器的第n个元素并在中间添加/删除元素,则对所有三个容器进行基准测试.
  • 如果您具有C++ 0x功能并且正在使用list但是您从不在列表中向后移动,那么您可以forward_list根据自己的喜好找到更多内容.它不会更快,但会占用更少的空间.

请注意,此建议变得更适用于容器越大.对于较小的容器,vector可能总是正确的选择,因为较低的常数因素.如有疑问,请以基准为准.

关联容器

  • 如果您没有TR1,C++ 0x或特定于供应商的unordered_foo/ hash_foo,则没有多少选择.使用四个容器中的任何一个都适合您的需要.
  • 如果您有unordered_foo,请使用它而不是有序版本,如果您不关心元素的顺序,并且您对该类型有良好的散列函数.

明智地使用例外

  • 不要在正常的代码路径中使用异常.当你遇到特殊情况时,请保存它们.

爱模板

  • 模板将在编译时和空间方面花费您的成本,但如果您有在运行时执行的计算,则性能提升可能会很惊人; 有时甚至像堕落者那样微妙.

避免 dynamic_cast

  • dynamic_cast有时是做某事的唯一选择,但通常dynamic_cast可以通过改进设计来消除使用.
  • 不要替换dynamic_cast为a typeid后面的static_cast任何一个.