现代C++编译器的有效优化策略

use*_*715 45 c++ optimization x86

我正在研究对性能至关重要的科学代码.该代码的初始版本已经编写和测试,现在,有了分析器,现在是时候从热点开始剃须周期了.

众所周知,编译器现在可以更有效地处理一些优化,例如循环展开,而不是手工编程的程序员.哪些技术还值得?显然,我会通过一个分析器来运行我尝试的所有内容,但是如果有传统的智慧关于什么往往有效,哪些无效,这将为我节省大量时间.

我知道优化非常依赖于编译器和体系结构.我正在使用针对Core 2 Duo的英特尔C++编译器,但我也对gcc或"任何现代编译器"的效果感兴趣.

以下是我正在考虑的一些具体想法:

  • 用手工卷取代STL容器/算法有什么好处?特别是,我的程序包括一个非常大的优先级队列(当前是a std::priority_queue),其操作占用了大量的总时间.这是值得研究的事情,还是STL实施可能是最快的?
  • 类似地,对于std::vector需要大小未知但上限相当小的s,用静态分配的数组替换它们是否有利可图?
  • 我发现动态内存分配通常是一个严重的瓶颈,消除它会导致显着的加速.因此,我很有兴趣通过值返回大型临时数据结构与通过指针返回相对于通过引用传递结果的性能权衡.有没有办法可靠地确定编译器是否会为给定方法使用RVO(假设调用者当然不需要修改结果)?
  • 编译器的缓存感知如何?例如,是否值得研究重新排序嵌套循环?
  • 鉴于该程序的科学性,浮点数被用于各处.我的代码中的一个重要瓶颈曾经是从浮点到整数的转换:编译器会发出代码来保存当前的舍入模式,更改它,执行转换,然后恢复旧的舍入模式---即使程序中没有任何内容永远改变了舍入模式!禁用此行为会大大加快我的代码速度.我应该注意哪些与浮点相关的类似问题?
  • C++被单独编译和链接的一个结果是编译器无法进行看起来非常简单的优化,例如在循环的终止条件下移动方法调用如strlen().有没有像我这样的优化,因为它们不能由编译器完成,必须手工完成?
  • 另一方面,我是否应该避免使用任何技术,因为它们可能会干扰编译器自动优化代码的能力?

最后,将某些类型的答案扼杀在萌芽状态:

  • 我知道优化在复杂性,可靠性和可维护性方面具有成本.对于此特定应用,提高性能值得这些成本.
  • 我知道最好的优化通常是改进高级算法,而且这已经完成了.

Ste*_*hen 26

用手工卷取代STL容器/算法有什么好处?特别是,我的程序包含一个非常大的优先级队列(当前是std :: priority_queue),其操作占用了大量的总时间.这是值得研究的事情,还是STL实施可能是最快的?

我假设您知道STL容器依赖于复制元素.在某些情况下,这可能是一个重大损失.存储指针,如果你进行了大量的容器操作,你可能会看到性能提升.另一方面,它可能会减少缓存局部性并伤害您.另一种选择是使用专门的分配器.

某些容器(例如map,set,list)依靠大量的指针操作的.虽然违反直觉,但它往往会导致更快的代码替换它们vector.生成的算法可能来自O(1)O(log n)来自O(n),但由于缓存局部性,它在实践中可以更快.配置文件,以确保.

你提到你正在使用priority_queue,我想这可以为重新安排元素付出很多,特别是如果它们很大的话.您可以尝试切换底层容器(可能deque或专门).我几乎可以肯定存储指针 - 再次,配置文件是肯定的.

沿着类似的路线,对于需要大小未知但上限相当小的std :: vectors,用静态分配的数组替换它们是否有利可图?

同样,这可能会有所帮助,具体取决于用例.您可以避免堆分配,但前提reserve()是您不需要阵列超过堆栈...或者您可以调整大小,vector以便重新分配时复制更少.

我发现动态内存分配通常是一个严重的瓶颈,消除它会导致显着的加速.因此,我很有兴趣通过值返回大型临时数据结构与通过指针返回相对于通过引用传递结果的性能权衡.有没有办法可靠地确定编译器是否会为给定方法使用RVO(假设调用者当然不需要修改结果)?

您可以查看生成的程序集以查看是否应用了RVO,但是如果返回指针或引用,则可以确定没有副本.这是否有用取决于你正在做什么 - 例如,不能返回对临时工具的引用.您可以使用竞技场来分配和重用对象,这样就不会支付大量的惩罚.

编译器的缓存感知如何?例如,是否值得研究重新排序嵌套循环?

我在这个领域看到了戏剧性的(非常戏剧性的)加速.我看到了比以后通过多线程处理代码看到的更多改进.自那以后的五年里,情况可能发生了变化 - 只有一种方式可以肯定 - 形象.

另一方面,我是否应该避免使用任何技术,因为它们可能会干扰编译器自动优化代码的能力?

  • 使用explicit你的单参数构造函数.临时对象构造和销毁可能隐藏在您的代码中.

  • 注意大对象上隐藏的复制构造函数调用.在某些情况下,请考虑使用指针替换.

  • 简介,个人资料,个人资料 调整瓶颈区域.


lea*_*der 20

看一下面向对象编程幻灯片的优秀陷阱,了解有关重构地方代码的一些信息.根据我的经验,获得更好的地方几乎总是最大的胜利.

一般过程:

  • 学习喜欢调试器中的反汇编视图,或者让构建系统生成中间汇编文件(.s),如果可能的话.密切关注变化或看起来令人震惊的事情 - 即使不熟悉给定的指令集架构,您也应该能够清楚地看到一些事情!(我有时检查一系列具有相应.cpp/.c更改的.s文件,只是为了利用我的SCM中可爱的工具来观察代码和相应的asm随时间的变化.)
  • 获取可以监视CPU性能计数器的分析器,或者至少可以猜测缓存未命中.(AMD CodeAnalyst,cachegrind,vTune等)

其他一些具体的事情:

  • 理解严格的别名. 完成后,restrict如果您的编译器有它,请使用它.(也在这里检查一下痉挛!)
  • 检查处理器和编译器上的不同浮点模式.如果您不需要非规范化范围,则选择不使用此模式的模式可以获得更好的性能.(听起来你已经在这个领域做了一些事情,基于你对舍入模式的讨论.)
  • 绝对避免allocs: 调用reservestd::vector的时候就可以了,或者使用std::array时,你知道在编译时的大小.
  • 使用内存池来增加位置并减少alloc/free开销; 还要确保缓存行对齐并防止ping-ponging.
  • 如果您以可预测的模式分配事物,则使用帧分配器,并且可以一次性解除分配所有内容.
  • 千万要注意不变量.你知道的不变的东西可能不是编译器,例如在循环中使用struct或class成员.我发现在这里找到正确习惯的最简单方法是给所有东西命名,并且更喜欢在循环之外命名.例如const int threshold = m_currentThreshold;或者Thing * const pThing = pStructHoldingThing->pThing; 幸运的是,你通常可以在反汇编视图中看到需要这种处理的东西.这也有助于稍后调试(使得watch/locals窗口在调试版本中表现得更好)!
  • 如果可能,避免在循环中写入 - 先累积,然后再写入或批量写入一些.YMMV,当然.

WRT您的std::priority_queue问题:将内容插入向量(priority_queue的默认后端)往往会移动很多元素.如果你可以分成几个阶段,在那里插入数据,然后对它进行排序,然后在它排序后读取它,你可能会好多了.虽然你肯定会失去局部性,但你可能会发现一个更自我排序的结构,如std :: map或std :: set值得开销 - 但这实际上取决于你的使用模式.


Tho*_*ews 13

用手工卷取代STL容器/算法有什么好处?
我只会将此视为最后一种选择.STL容器和算法已经过全面测试.就开发时间而言,创建新的开销是昂贵的.

沿着类似的路线,对于需要大小未知但上限相当小的std :: vectors,用静态分配的数组替换它们是否有利可图?
首先,尝试为矢量保留空间.看看std::vector::reserve方法.不断增长或更改为更大尺寸的矢量将浪费动态内存和执行时间.添加一些代码以确定上限的良好值.

我发现动态内存分配通常是一个严重的瓶颈,消除它会导致显着的加速.因此,我很有兴趣通过值返回大型临时数据结构与通过指针返回相对于通过引用传递结果的性能权衡.有没有办法可靠地确定编译器是否会为给定方法使用RVO(假设调用者当然不需要修改结果)?
原则上,始终通过引用或指针传递大型结构.喜欢通过不断的参考.如果您使用指针,请考虑使用智能指针.

编译器的缓存感知如何?例如,是否值得研究重新排序嵌套循环?
现代编译器非常了解指令缓存(管道)并试图防止它们被重新加载.你总是可以通过编写使用较少的分支(从代码帮助你的编译器if,switch,循环结构函数调用).

通过调整程序来优化数据缓存,您可能会看到更显着的性能提升.在网上搜索数据驱动设计.关于这个主题有很多优秀的文章.

鉴于该程序的科学性,浮点数被用于各处.我的代码中的一个重要瓶颈曾经是从浮点到整数的转换:编译器会发出代码来保存当前的舍入模式,更改它,执行转换,然后恢复旧的舍入模式---即使程序中没有任何内容永远改变了舍入模式!禁用此行为会大大加快我的代码速度.我应该注意哪些与浮点相关的类似问题?
为了准确,请保持一切double.仅在必要时以及可能在显示之前调整圆角.这属于优化规则,使用更少的代码,消除无关或死木代码.

另请参阅上面有关在使用容器之前保留容器空间的部分.

某些处理器可以更快或更快地加载和存储浮点数.这需要在优化之前收集配置文件数据.但是,如果您知道分辨率最低,则可以使用整数并将基数更改为最小分辨率.例如,在处理美元货币时,整数可用于表示1美元或1/1000美元.

C++被单独编译和链接的一个结果是编译器无法进行看起来非常简单的优化,例如在循环的终止条件下移动方法调用如strlen().有没有像我这样的优化,因为它们不能由编译器完成,必须手工完成?
这是一个错误的假设.编译器可以根据函数的签名进行优化,尤其是在参数正确使用的情况下const.我总是喜欢通过在循环之外移动常量来帮助编译器.对于上限值,例如字符串长度,请const在循环之前将其分配给变量.该const修改将有助于优化.

循环中始终存在倒计时优化.对于许多处理器,寄存器上跳跃等于零比较更有效,如果小于,则跳跃.

另一方面,我是否应该避免使用任何技术,因为它们可能会干扰编译器自动优化代码的能力?
我会避免"微观优化".如果您有任何疑问,请在最高优化设置下打印出编译器生成的汇编代码(对于您正在质疑的区域).尝试重写代码以表达编译器的汇编代码.如果可以的话,优化此代码.更多内容需要特定于平台的说明.

优化理念和概念

1.计算机更喜欢执行顺序指令.
分支扰乱了他们.一些现代处理器有足够的指令缓存来包含小循环的代码.如有疑问,请勿引起分支.

2.消除需求
减少代码,提高性能.

3.在代码之前优化设计 通常,通过改变设计而不是改变设计的实现,可以获得更多的性能.较少的设计可以减少代码,提高性能.

4.考虑数据组织 优化数据.
将常用字段组织成substructures.设置数据大小以适合数据高速缓存行.从数据结构中删除常量数据.尽可能
使用const说明符.

5.考虑页面交换 操作系统会将您的程序或任务换成另一个程序或任务.经常进入硬盘上的"交换文件".将代码分解为包含大量执行代码和较少执行代码的块将有助于操作系统.此外,将大量使用的代码凝聚成更紧密的单元.这个想法是减少硬盘驱动器中代码的交换(例如获取"远"功能).如果必须换掉代码,它应该作为一个单元.

6.考虑I/O优化 (包括文件I/O).
大多数I/O更喜欢将大块数据减少到许多小块数据.硬盘驱动器喜欢继续旋转.较大的数据包比较小的数据包具有更少的开销.
将数据格式化为缓冲区然后写入缓冲区.

7.消除竞争
摆脱与您的处理器应用程序竞争的任何程序和任务.病毒扫描和播放音乐等任务.甚至I/O驱动程序也需要一个操作(这就是为什么要减少数量或I/O事务).

这些应该让你忙一阵子.:-)


Ama*_*9MF 7

  1. 与动态分配相比,使用内存缓冲池可以获得很好的性能优势.如果它们在长时间执行运行中减少或防止堆碎片,则更是如此.

  2. 注意数据位置.如果您有本地数据与全局数据的重要组合,则可能会使缓存机制过度使用.尽量使数据集保持紧密,以最大限度地利用缓存行有效性.

  3. 尽管编译器在循环方面做得非常出色,但在性能调优时我仍然仔细检查它们.您可以发现在编译器只能修剪百分比的情况下产生数量级的架构缺陷.

  4. 如果单个优先级队列在其操作中使用了大量时间,则创建表示优先级桶的队列的电池可能是有益的.在这种情况下,以速度交易会很复杂.

  5. 我注意到你没有提到使用SSE类型指令.它们适用于您的数字运算吗?

祝你好运.


Dou*_*rie 5

是一篇关于这个主题的好文章.


Las*_*lan -1

如果您正在进行繁重的浮点数学运算,如果这能很好地映射您的问题,您应该考虑使用 SSE 来矢量化您的计算。

Google SSE 内在函数以获取有关此内容的更多信息。