我们还应该优化“小”吗?

我正在改变我的for循环以使用++i而不是i++来增加并且开始思考,这是否真的有必要了? 当然,今天的编译器会自己进行优化。

在本文中, http://leto.net/docs/C-optimization.php ,从1997年开始迈克尔·李进入其他优化,如内联,循环展开,循环干扰,循环反转,强度降低等等。 这些仍然相关吗?

我们应该进行哪些低级代码优化,以及我们可以安全地忽略哪些优化?

编辑:这与过早优化无关。 已经做出了优化的决定。 现在问题是什么是最有效的方法。

轶事:我曾经审查了一个要求规范:“程序员应该离开一个而不是乘以2”。

如果优化没有成本,那就去做吧。 在编写代码时, ++ii++一样容易编写,所以更喜欢前者。 没有成本。

另一方面, 之后返回并进行此更改需要时间,并且很可能不会产生显着差异,因此您可能不应该为此烦恼。

但是,是的,它可以有所作为。 在内置类型上,可能不是,但对于复杂类,编译器不太可能能够优化它。 这样做的原因是增量操作no不再是内置于编译器中的内部操作,而是类中定义的函数。 编译器可以像任何其他函数一样优化它,但它通常不能假设可以使用预增量而不是后增量。 这两个function可能完全不同。

因此,在确定编译器可以执行哪些优化时,请考虑它是否有足够的信息来执行它。 在这种情况下,编译器不知道后增量和预增量对对象执行相同的修改,因此它不能假设可以用另一个替换。 但是你有这方面的知识,所以你可以安全地进行优化。

您提到的许多其他内容通常可以通过编译器非常有效地完成:内联可以由编译器完成,并且它通常比您更好。 它需要知道的是,函数的一部分function有多大,包括函数调用,以及调用它的频率是多少? 通常可能不应该内联一个被调用的大函数,因为最终会复制大量代码,从而导致更大的可执行文件和更多指令缓存未命中。 内联总是一种权衡,通常,编译器在权衡所有因素方面比你更好。

循环展开是一种纯粹的机械操作,编译器可以轻松完成。 强度降低同样如此。 交换内部循环和外部循环比较棘手,因为编译器必须certificate改变的遍历顺序不会影响结果,这很难自动完成。 所以这是你应该自己做的优化。

但即使在编译器能够执行的简单操作中,您有时也会得到编译器没有的信息。 如果您知道某个函数将被频繁调用,即使它只从一个地方调用,也可能值得检查编译器是否自动内联它,如果不是则手动执行。

有时您可能比编译器更了解循环(例如,迭代次数总是4的倍数,因此您可以安全地将其展开4次)。 编译器可能没有此信息,因此如果要内联循环,则必须插入一个epilog以确保最后几次迭代正确执行。

因此,如果1)您确实需要性能,并且2)您拥有编译器没有的信息,那么这样的“小规模”优化仍然是必要的。

在纯粹的机械优化上,你无法胜过编译器。 但是你可能能够做出编译器无法做出的假设, 就是你能够比编译器更好地进行优化。

这是一个陈旧的主题,SO包含大量的好的和坏的建议。

让我告诉你我从做过性能调优的经验中找到了什么。

在性能和其他内容(如内存和清晰度)之间存在权衡曲线,对吗? 并且你会期望获得更好的性能,你必须给予一些东西,对吗?

只有当程序处于权衡曲线时才会这样。 大多数软件,如最初编写的, 距离权衡曲线几英里远 。 大多数时候,谈论放弃一件事来获得另一件事是无关紧要和无知的。

我使用的方法不是测量,而是诊断 。 我不关心各种例程的速度有多快或它们被调用的次数。 我想确切地知道哪些指令导致缓慢,以及为什么

大规模软件工作(不是一个人的小项目)表现不佳的主要和主要原因是普遍性 。 采用了太多的抽象层,每个抽象层都提取了性能损失。 这通常不是问题 – 直到它成为一个问题 – 然后它就是一个杀手。

所以我所做的就是一次解决一个问题。 我称之为“slu”“,简称”缓慢的错误“。 我删除的每个slug产生的速度从1.1x到10x不等,具体取决于它有多糟糕。 每个被移除的子弹使得剩余的子弹占用剩余时间的更大部分,因此它们变得更容易找到。 通过这种方式,可以快速处理所有“低悬的水果”。

那时,我知道什么是花费时间,但修复可能更困难,例如部分重新设计软件,可能通过删除无关的数据结构或使用代码生成。 如果可以做到这一点,那么可以引发新一轮的段塞移除,直到程序多次不仅比开始更快,而且更小更清晰。

我建议您自己获得这样的体验,因为当您设计软件时,您将知道该做什么,并且您将开始做更好(和更简单)的设计。 与此同时,你会发现自己与经验不足的同事不一致,他们在没有召唤十几个课程的情况下就无法开始考虑设计。

补充:现在,为了回答你的问题,当诊断说你有一个热点时,应该进行低级优化(即调用堆栈底部的一些代码出现在足够的调用堆栈样本上(10%或更多)被称为花费大量时间)。 如果热点在代码中,您可以编辑。 如果你在“新”,“删除”或字符串比较中有一个热点,那么在堆栈中向上看更高的东西来摆脱它。

希望有所帮助。

这些优化仍然具有现实意义。 关于您的示例,在内置算术类型上使用++ i或i ++无效。

在用户定义的递增/递减运算符的情况下,++ i是优选的,因为它并不意味着复制递增的对象。

所以一个好的编码风格是在for循环中使用前缀增量/减量。

一般来说,没有。 在整个代码库中,编译器可以更好地进行这样的小型,直接的微优化。 通过使用正确的优化标志编译发布版本,确保在此处启用编译器。 如果您使用Visual Studio,您可能希望尝试支持大小超速(有很多小代码更快的情况),链接时代码生成(LTCG,它使编译器能够进行跨代码优化),甚至可能是配置文件引导的优化。

您还需要记住,从性能角度来看,大量代码无关紧要 – 优化此代码将没有用户可见的效果。

您需要尽早定义您的绩效目标并经常衡量,以确保您满足这些目标。 如果超出目标,请使用分析器等工具来确定代码中的热点位置并进行优化。

正如这里提到的另一张海报,“没有测量和理解的优化根本不是优化 – 它只是随机变化。”

如果您已经测量并确定特定函数或循环是热点,则有两种方法可以优化它:

  • 首先,通过减少昂贵代码的调用,在更高级别优化它。 这通常会带来最大的好处。 算法级别的改进属于这个级别 – 算法将更好的大O应该导致运行热点代码更少。
  • 如果无法减少呼叫,那么您应该考虑微优化。 查看编译器正在发出的实际机器代码,并确定它正在做什么,这是最昂贵的 – 如果事实certificate正在发生复制临时对象,那么考虑前缀++而不是postfix。 如果它在循环开始时进行不必要的比较,则将循环翻转为do / while,依此类推。 如果不理解为什么代码很慢,任何全面的微优化都是无用的。

是的,那些东西仍然相关。 我做了一些这样的优化但是,公平地说,我主要编写的代码必须在ARM9上大约10ms内执行相对复杂的操作。 如果您正在编写在更现代的CPU上运行的代码,那么好处将不会那么大。

如果您不关心可移植性,并且您正在进行相当多的数学运算,那么您可能还会考虑使用目标平台上可用的任何矢量运算 – x86上的SSE,PPC上的Altivec。 如果没有很多帮助,编译器就无法轻松使用这些指令,而且内部函数现在很容易使用。 您链接到的文档中没有提到的另一件事是指针别名。 如果您的编译器支持某种“restrict”关键字,您有时可以获得良好的速度提升。 当然,考虑缓存使用情况也很重要。 与优化掉奇数副本或展开循环相比,以充分利用缓存的方式重新组织代码和数据可以显着提高速度。

然而,与以往一样,最重要的是要描述。 只优化实际上很慢的代码,确保优化实际上更快,并查看反汇编,看看编译器在您尝试改进之前已经为您做了哪些优化。

不好的例子 – 是否使用++ii++的决定不涉及任何forms的权衡! ++i有(可能有)净利益而没有任何缺点 。 有许多类似的场景,在这些领域的任何讨论都是浪费时间。

也就是说,我相信知道目标编译器在多大程度上能够优化小代码片段非常重要 。 事实是:现代编译器(有时令人惊讶!)擅长它。 Jason有一个关于优化(非尾递归)阶乘函数的令人难以置信的故事 。

另一方面,编译器也可能令人惊讶地愚蠢。 关键是许多优化需要控制流分析才能完成NP。 因此,优化成为编译时间和有用性之间的权衡。 通常,优化的局部性起着至关重要的作用,因为当编译器所考虑的代码大小仅增加几个语句时,执行优化所需的计算时间会增加太多。

正如其他人所说的那样,这些微小的细节仍然具有相关性,并且总是如此(对于可预见的未来)。 虽然编译器一直变得更聪明,机器变得更快,但数据的大小也在增长 – 事实上,我们正在失去这场特殊的战斗; 在许多领域,数据量的增长要比计算机变得更好。

对于C程序员来说,你列出的所有优化实际上都是无关紧要的 – 编译器在执行内联,循环展开,循环干扰,循环反转和强度降低等方面好得多。

关于++ii++ :对于整数,它们生成相同的机器代码,因此您使用的是风格/偏好问题。 在C ++中,对象可以重载那些前后增量运算符,在这种情况下,通常最好使用preincrement,因为postincrement需要额外的对象副本。

至于使用移位而不是乘以2的乘法,编译器已经为你做了那个。 根据架构,它可以做更多聪明的事情,例如将乘法乘以5转换为x86上的单个lea指令。 但是,如果除以2的幂的除法和模数,则可能需要更多关注以获得最佳代码。 假设你写:

 x = y / 2; 

如果xy是有符号整数,则编译器不能将其转换为右移,因为它会对负数产生错误的结果。 因此,它会发出正确的移位和一些麻烦的指令,以确保结果对于正数和负数都是正确的。 如果你知道xy总是正数,那么你应该帮助编译器输出并改为使用无符号整数。 然后,编译器可以将其优化为单个右移指令。

模数运算符%工作方式类似 – 如果你使用2的幂进行修改,使用有符号整数,编译器必须发出一个and指令加上一点点,以使结果对正数和负数正确,但它可以如果处理无符号数字,则发出单个指令。

肯定是的,因为编译器需要更多资源来优化未优化的代码,而不是优化已优化的东西。 特别是,它会使计算机消耗更多的能量,尽管它很小,但仍会对已经受到伤害的性质造成不良影响。 这对于开源代码尤其重要,开源代码比闭源代码编译得更频繁。

走向绿色,拯救地球,优化自己

做正确,然后快速 – 基于性能测量。

很好地选择算法并尽可能以最简单的方式实现它们。 只有当你的用户说你的表现在言语或行动上是不可接受时,才能牺牲性能的可读性。

正如唐纳德克努特/托尼霍尔所说的那样“过早优化是所有邪恶的根源” – 现在30年后仍然如此……

上次我在用于STL迭代器的Microsoft C ++编译器上测试++ it和it ++时,它发出的代码较少,所以如果你处于一个大规模的循环中,那么使用++它可能会获得很小的性能提升。

对于整数等,编译器将发出相同的代码。

编译器可以更好地判断和做出这样的决定。 您所做的微观优化可能会受到影响,并最终错过了重点。

多年来我一直有一个有趣的观察结果是,一代后代的优化代码实际上在下一代中实际上是反优化的。 这是因为处理器实现发生了变化,以至于if / else成为现代CPU的瓶颈,管道很深。 我会说干净,简洁,简洁的代码通常是最好的最终结果。 优化真正重要的地方在于数据结构,以使它们正确而纤薄。

有三个引用我相信每个开发人员都应该知道优化 – 我首先在Josh Bloch的“Effective Java”一书中读到它们:

更多的计算罪是以效率的名义(不一定实现它)而不是任何其他单一原因 – 包括盲目的愚蠢。

( William A. Wulf )

我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源。

( Donald E. Knuth )

我们在优化方面遵循两条规则:

规则1:不要这样做。

规则2 :(仅限专家)。 不要这样做 – 也就是说,直到你有一个完全清晰和未经优化的解决方案。

( MAjackson )

所有这些报价都是(AFAIK)至少20到30年,这个时代的CPU和内存意味着比今天更多。 我认为开发软件的正确方法是首先要有一个可行的解决方案,然后使用分析器来测试性能瓶颈在哪里。 一位朋友曾告诉我一个用C ++和Delphi编写的应用程序,并且存在性能问题。 使用分析器,他们发现应用程序花了相当多的时间将字符串从Delphi的结构转换为C ++,反之亦然 – 没有微优化可以检测到……

总而言之,不要认为您知道性能问题将在何处。 为此使用分析器。

不要试图猜测你的编译器在做什么。 如果您已经确定需要在此级别优化某些内容,请隔离该位并查看生成的程序集。 如果您可以看到生成的代码正在做一些可以改进的缓慢的事情,那么无论如何都要在代码级别处理它,看看会发生什么。 如果你真的需要控制,请在汇编中重写该位并将其链接。

这是一个痛苦的屁股,但唯一的方法是真正看到正在发生的事情。 请注意,一旦您更改任何内容(不同的CPU,不同的编译器,甚至不同的缓存等),所有这些严格的优化都可能变得无用,而且这是沉没成本。

还需要注意的是,从前/后递增/递减操作符改变不会引起不希望的副作用。 例如,如果你循环遍历循环5次只是为了多次运行一组代码而对循环索引值没有任何兴趣,你可能没问题(YMMV)。 另一方面,如果您访问循环索引值而不是结果可能不是您所期望的:

 #include  int main() { for (unsigned int i = 5; i != 0; i--) std::cout << i << std::endl; for (unsigned int i = 5; i != 0; --i) std::cout << "\t" << i << std::endl; for (unsigned int i = 5; i-- != 0; ) std::cout << i << std::endl; for (unsigned int i = 5; --i != 0; ) std::cout << "\t" << i << std::endl; } 

结果如下:

 5 4 3 2 1 5 4 3 2 1 4 3 2 1 0 4 3 2 1 

前两种情况没有显示出差异,但请注意,通过切换到预递减运算符来尝试“优化”第四种情况会导致迭代完全丢失。 不可否认,这是一个有点人为的案例,但是当我以相反的顺序(即从头到尾)遍历一个数组时,我已经看到了这种循环迭代(第三种情况)。

我想补充一点。 这种“过早优化是坏事”是一种垃圾。 选择算法时你会怎么做? 您可能会选择具有最佳时间复杂度的一个 – OMG过早优化。 然而每个人看起来都很好。 所以看起来真实的态度是“过早的优化是坏的 – 除非你按我的方式去做”在一天结束时,做你需要做的任何事情来制作你需要做的应用程序。

“程序员应该离开一个而不是乘以2”。 希望你不要想乘以花车或负数;)

正如其他人所说,如果我是某个对象的实例,++ i可能比i ++更有效。 这种差异可能对您有意义,也可能不重要。

但是,在您关于编译器是否可以为您执行这些优化的问题的上下文中,在您选择的示例中它不能。 原因是++ i和i ++具有不同的含义 – 这就是为什么它们被实现为不同的function。 i ++必须做额外的工作(在递增之前复制当前状态,执行增量,然后返回该状态)。 如果您不需要额外的工作,那么为什么选择其他更直接的forms? 答案可能是可读性 – 但在这种情况下,在C ++中编写++ i已经成为惯用语,因此我不相信可读性。

因此,如果在编写执行不必要的额外工作的代码(可能或可能不重要)之间做出选择,而本身没有任何好处,我总是会选择更直接的forms。 这不是一个不成熟的优化。 另一方面,通常也不足以使宗教信仰。

当然, 当且仅当它导致该特定程序的实际改进足够值得编码时间,任何可读性降低等等。我认为你不能在所有程序中为此制定规则,或者真的适合任何优化。 它完全取决于特定情况下的实际情况。

像++ i这样的东西,时间和可读性的权衡是如此微小,如果它实际上导致了改进,那么它可能值得养成习惯。

只有你确定他们是相关的。 这意味着您之前已经在特定编译器上调查了此问题,或者您已经完成了以下操作:

  1. 生成function代码
  2. 分析了那段代码
  3. 确定了瓶颈
  4. 简化设计以消除瓶颈
  5. 选择最小化调用瓶颈的算法

如果你已经完成了所有这些事情,那么通常做的最好的事情就是让你的编译器发出一些你可以自己检查的低级别(如汇编)并根据它做出具体的判断。 根据我的经验,每个编译器都有一点不同。 有时,对一个进行优化会导致另一个进行优化,从而生成效

如果你还没有完成这些事情,那么我称之为过早优化,我建议不要这样做。 在做这些事情之前进行优化会带来与所涉及的成本相比不成比例地小的奖励。

  • 除非我在嵌入式设备上书写,否则我通常不会优化低于O(f(n))的复杂度。

  • 对于典型的g ++ / Visual Studio工作,我假设基本的优化将可靠地进行(至少在请求优化时)。 对于不太成熟的编译器,该假设可能无效。

  • 如果我在数据流上做大量数学工作,我会检查编译器发出SIMD指令的能力。

  • 我宁愿使用不同算法调整代码,而不是特定编译器的特定版本。 算法将经受多个处理器/编译器的考验,而如果你调整2008 Visual C ++(第一个版本)版本,你的优化可能甚至不适用于明年。

  • 在旧计算机中非常合理的某些优化技巧certificate今天存在问题。 例如,++ / ++运算符是围绕旧架构设计的,该架构具有非常快的增量指令。 今天,如果你做的事情

    for(int i = 0; i < top; i+=1)

    我认为编译器会将i+=1优化为inc指令(如果CPU有)。

  • 经典的建议是优化自上而下。

首先 – 始终运行分析以检查。

首先,如果您优化正确的代码部分。 如果代码运行总时间的1% – 忘了。 即使你把它加速了50%,你也可以获得0.5%的总加速。 除非你正在做一些奇怪的加速会慢得多(特别是如果你使用了良好的优化编译器)。 其次,如果你正确地优化它。 哪个代码在x86上运行得更快?

 inc eax 

要么

 add eax, 1 

好。 据我所知,在早期的处理器中第一个处理器,但在第二个处理器P4处(如果这些特定指令运行得更快或更慢,则无关紧要的一点是它一直在变化)。 编译器可能会对这些更改进行更新 – 您不会。

In my opinion the primary target is the optimalizing that cannot be performed by compilator – as mentioned earlier the data size (you may think that it is not needed on nowadays 2 GiB computers – but if your data is bigger then processor cache – it will run much slowler).

In general – do it only if you must and/or you know what you are doing. It will require an amount of knowledge about the code, compiler and the low-level computer architecture that is not metioned in the question (and to be honest – I do not posess). And it will likely gain nothing. If you want to optimize – do it on more highier level.

I still do things like ra<<=1; instead of ra*=2; And will continue to. But the compilers (as bad as they are) and more importantly the speed of the computers is so fast that these optimizations are often lost in the noise. As a general question, no it is not worth it, if you are specifically on a resource limited platform (say a microcontroller) where every extra clock really counts then you probably already do this and probably do a fair amount of assembler tuning. As a habit I try not to give the compiler too much extra work, but for code readability and reliability I dont go out of my way.

The bottom line for performance though has never changed. Find some way to time your code, measure to find the low hanging fruit and fix it. Mike D. hit the nail on the head in his response. I have too many times seen people worry about specific lines of code not realizing that they are either using a poor compiler or by changing one compiler option they could see several times increase in execution performance.