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

时间:2009-04-18 15:58:17

标签: c++ c optimization

我正在使用++i而不是i++将我的for循环更改为增量并开始思考,这是否真的有必要了?当然,今天的编译器会自行完成这项优化。

在这篇文章http://leto.net/docs/C-optimization.php中,自1997年以来,Michael Lee进行了其他优化,例如内联,循环展开,循环干扰,循环反转,强度降低等等。这些仍然相关吗?

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

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

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

22 个答案:

答案 0 :(得分:22)

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

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

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

只有当程序在权衡曲线时才会出现。大多数软件,如首次编写,距离权衡曲线数英里。大多数时候,谈论放弃一件事来换取另一件事是无关紧要和无知的。

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

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

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

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

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

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

希望有所帮助。

答案 1 :(得分:17)

如果优化没有成本,请执行此操作。在编写代码时,++ii++一样容易编写,所以更喜欢前者。没有成本。

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

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

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

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

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

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

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

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

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

答案 2 :(得分:10)

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

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

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

答案 3 :(得分:8)

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

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

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

正如另一张海报here所提到的,“没有衡量和理解的优化根本就不是优化 - 只是随机变化。”

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

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

答案 4 :(得分:7)

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

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

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

答案 5 :(得分:7)

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

那就是说,我相信知道目标编译器能够优化小代码片段 到什么程度非常重要。事实是:现代编译器(有时令人惊讶!)擅长它。 Jason有一个关于优化(非尾递归)因子函数的incredible story

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

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

答案 6 :(得分:6)

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

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

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

x = y / 2;

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

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

答案 7 :(得分:3)

当然可以,因为编译器需要更多资源来优化未优化的代码,而不是优化已优化的内容。特别是,它会使计算机消耗更多的能量,尽管它很小,仍会对已经受到伤害的性质造成不良影响。这对于开源代码尤其重要,开源代码的编译频率高于封闭源代码。

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

答案 8 :(得分:2)

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

答案 9 :(得分:2)

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

#include <iostream>

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

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

答案 10 :(得分:2)

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

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

答案 11 :(得分:2)

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

  

更多的计算罪行已经提交   效率的名称(没有   必须实现它)而不是任何   其他单一原因 - 包括失明   愚蠢。

William A. Wulf

  

我们应该忘记小事   效率,约占97%   时间:过早优化是   万恶之源。

Donald E. Knuth

  

我们遵循两条规则   优化:

     

规则1:不要这样做。

     

规则2 :(仅限专家)。不要这样做   它 - 但是,直到你有一个   完全清晰,未经优化   溶液

M. A. Jackson

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

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

答案 12 :(得分:2)

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

答案 13 :(得分:2)

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

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

答案 14 :(得分:2)

做正确,然后快速 - 基于绩效衡量。

选择算法并尽可能以最简单的方式实现它们。只有当你的用户说你的表现是不可接受的,无论是用语言还是用他们的行为时,都要追求性能的可读性。

正如Donald Knuth / Tony Hoare所说的那样“过早优化是所有邪恶的根源” - 30年后仍然如此......

答案 15 :(得分:1)

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

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

答案 16 :(得分:1)

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

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

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

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

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

    的事情

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

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

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

答案 17 :(得分:1)

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

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

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

答案 18 :(得分:1)

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

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

答案 19 :(得分:1)

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

  1. 制作了功能代码
  2. 描述了该代码
  3. 确定了瓶颈
  4. 简化设计以消除瓶颈
  5. 选择最小化调用瓶颈的算法
  6. 如果你已经完成了所有这些事情,那么通常做的最好的事情就是让你的编译器发出一些你可以自己检查的低级别(比如汇编)并根据它做出具体的判断。根据我的经验,每个编译器都有一点不同。有时,对一个进行优化会导致另一个进行优化,从而产生效率更低的代码。

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

答案 20 :(得分:0)

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

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

inc eax

add eax, 1

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

在我看来,主要目标是编译器不能执行的最优化 - 如前面提到的数据大小(您可能认为现在2 GiB计算机不需要它 - 但如果您的数据大于处理器缓存 - 它运行得慢得多。)

一般情况下 - 只有在您必须和/或您知道自己在做什么的情况下才能这样做。它需要大量关于代码,编译器和低级计算机体系结构的知识,而这些知识在问题中没有提到(说实话 - 我不赞成)。它可能一无所获。如果你想进行优化 - 在更高的层次上进行优化。

答案 21 :(得分:0)

我仍然会做ra&lt;&lt; = 1;而不是ra * = 2;并将继续。但是编译器(虽然它们是坏的)以及更重要的计算机速度是如此之快,以至于这些优化通常会在噪声中丢失。作为一般性问题,不值得,如果你是专门在资源有限的平台(比如微控制器),每个额外的时钟真的很重要,那么你可能已经这样做了,可能做了相当多的汇编调整。作为一种习惯,我尽量不给编译器额外的工作,但为了代码的可读性和可靠性,我不会忘记。

性能的底线从未改变过。找到一些方法来计算你的代码,测量找到低悬的果实并修复它。 Mike D.在他的回答中击中了头部。我有太多次看到人们担心特定的代码行没有意识到他们要么使用糟糕的编译器,要么通过更改一个编译器选项,他们可以看到执行性能提高了几倍。