我们还应该优化"小"吗?

Mar*_*ith 16 c c++ optimization

我正在改变我的for循环以增加使用++i而不是i++思考,这是否真的有必要了?当然,今天的编译器会自行完成这项优化.

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

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

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

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

Mik*_*vey 22

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

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

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

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

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

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

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

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

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

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

希望有所帮助.

  • 抱歉.我猜你打了我的"常规"按钮.我遇到太多程序员,他们担心像++ i,编译器优化或虚拟功能的速度,当他们的设计导致30倍减速时. (3认同)

jal*_*alf 17

如果优化没有成本,那就去做吧.编写代码时,++i编写起来也很容易i++,所以更喜欢前者.没有成本.

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

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

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

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

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

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

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

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

在纯机械优化方面,你无法超越编译器.但是你可能会做出编译器无法做出的假设,就是你能够比编译器更好地进行优化.


Căt*_*tiș 10

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

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

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


Mic*_*ael 8

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

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

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

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

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

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


Jam*_*and 7

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

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

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


Kon*_*lph 7

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

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

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

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


Ada*_*eld 6

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

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

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

x = y / 2;
Run Code Online (Sandbox Code Playgroud)

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

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