繁体   English   中英

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

[英]Effective optimization strategies on modern C++ compilers

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

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

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

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

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

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

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

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

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

某些容器(例如mapsetlist )依赖于大量的指针操作。 虽然违反直觉,但它通常可以导致更快的代码用vector替换它们。 得到的算法可能从O(1)O(log n)O(n) ,但由于缓存局部性,它在实践中可以快得多。 配置文件,以确保。

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

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

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

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

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

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

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

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

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

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

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

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

一般过程:

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

其他一些具体的事情:

  • 理解严格的别名。 完成后,如果编译器有restrict ,请使用restrict (也在这里检查一下痉挛!)
  • 检查处理器和编译器上的不同浮点模式 如果您不需要非规范化范围,则选择不使用此模式的模式可以获得更好的性能。 (听起来你已经在这个领域做了一些事情,基于你对舍入模式的讨论。)
  • 绝对避免allocs:你可以在std::vector调用reserve ,或者在你知道编译时的大小时使用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值得开销 - 但这实际上取决于你的使用模式。

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

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

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

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

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

鉴于该程序的科学性,浮点数被用于各处。 我的代码中的一个重要瓶颈曾经是从浮点到整数的转换:编译器会发出代码来保存当前的舍入模式,更改它,执行转换,然后恢复旧的舍入模式---即使程序中没有任何内容永远改变了舍入模式! 禁用此行为会大大加快我的代码速度。 我应该注意哪些与浮点相关的类似问题?
为了准确,将所有内容保持为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事务)。

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

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

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

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

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

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

祝你好运。

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

关于STL容器。

这里的大多数人声称STL提供了容器算法中最快的实现之一。 而且我说的恰恰相反:对于最现实世界的场景,STL容器被视为产生了非常灾难性的表现

人们争论STL中使用的算法的复杂性 这里STL是好的:O(1)用于list / queue ,向量(摊销)和O(log(N))用于map 但这不是典型应用程序性能的真正瓶颈! 对于许多应用程序而言,真正的瓶颈是堆操作malloc / freenew / delete等)。

list上的典型操作只需几个CPU周期。 map - 几十,可能更多(这当然取决于缓存状态和日志(N))。 典型的堆操作从需求成本到数千(!!!)的CPU周期。 例如,对于多线程应用程序,它们还需要同步(互锁操作)。 此外,在某些操作系统(如Windows XP)上,堆函数完全在内核模式下实现。

因此,典型场景中STL容器的实际性能主要取决于它们执行的堆操作量。 在这里,他们是灾难性的。 不是因为他们的实施很差,而是因为他们的设计 也就是说,这是设计的问题。

另一方面,其他容器的设计也不同。 一旦我根据自己的需要设计和编写了这样的容器:

http://www.codeproject.com/KB/recipes/Containers.aspx

事实证明,从性能的角度来看,它不仅仅是优越的。

但最近我发现我并不是唯一一个想到这一点的人。 boost::intrusive是以类似于我当时的方式实现的容器库。

我建议你尝试一下(如果你还没有)

这是我用过的一些东西:

  • 模板来专门化最内层的循环边界(使它们非常快)
  • 使用__restrict__关键字来表示别名问题
  • 保留向量预先确定默认值。
  • 避免使用地图(它可能真的很慢)
  • vector append / insert可能会非常慢。 如果是这种情况,原始操作可能会使其更快
  • N字节内存对齐(英特尔已完全对齐, http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cppref_pragma_vector.htm
  • 试图将内存保留在L1 / L2缓存中。
  • 用NDEBUG编译
  • 使用oprofile配置文件,使用opannotate查找特定的行(stl开销明显可见)

这里是配置文件数据的示例部分(所以你知道在哪里寻找问题)

 * Output annotated source file with samples
 * Output all files
 *
 * CPU: Core 2, speed 1995 MHz (estimated)
--
 * Total samples for file : "/home/andrey/gamess/source/blas.f"
 *
 * 1020586 14.0896
--
 * Total samples for file : "/home/andrey/libqc/rysq/src/fock.cpp"
 *
 * 962558 13.2885
--
 * Total samples for file : "/usr/include/boost/numeric/ublas/detail/matrix_assign.hpp"
 *
 * 748150 10.3285

--
 * Total samples for file : "/usr/include/boost/numeric/ublas/functional.hpp"
 *
 * 639714  8.8315
--
 * Total samples for file : "/home/andrey/gamess/source/eigen.f"
 *
 * 429129  5.9243
--
 * Total samples for file : "/usr/include/c++/4.3/bits/stl_algobase.h"
 *
 * 411725  5.6840
--

我的项目中的代码示例

template<int ni, int nj, int nk, int nl>
inline void eval(const Data::density_type &D, const Data::fock_type &F,
                 const double *__restrict Q, double scale) {

    const double * __restrict Dij = D[0];
    ...
    double * __restrict Fij = F[0];
    ...

    for (int l = 0, kl = 0, ijkl = 0; l < nl; ++l) {
        for (int k = 0; k < nk; ++k, ++kl) {
            for (int j = 0, ij = 0; j < nj; ++j, ++jk, ++jl) {
                for (int i = 0; i < ni; ++i, ++ij, ++ik, ++il, ++ijkl) {

用手工卷取代STL容器/算法有什么好处吗?

一般情况下,除非你正在处理糟糕的实施。 我不会因为您认为可以编写更严格的代码而替换STL容器或算法。 只有当STL版本比你的问题需要的更通用时我才会这样做。 如果你可以编写一个更简单的版本来满足你的需求,那么可能会有一些速度可以获得。

我见过的一个例外是用一个不需要线程同步的std :: string替换一个copy-on-write std :: string。

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

不太可能。 但是如果你使用大量时间分配一定的大小,添加reserve()调用可能是有利可图的。

通过值返回大型临时数据结构与通过指针返回相对于通过引用传递结果的性能权衡。

使用容器时,我传递输入的迭代器和输出迭代器,这仍然非常通用。

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

不是特别的。 是。 我发现错过的分支预测和缓存恶意内存访问模式是性能的两个最大杀手(一旦你有了合理的算法)。 许多旧代码使用“早期”测试来减少计算。 但在现代处理器上,这通常比做数学和忽略结果更昂贵。

我的代码中的一个重要瓶颈曾经是从浮点到整数的转换

对。 我最近发现了同样的问题。

C ++被单独编译和链接的一个结果是编译器无法进行看起来非常简单的优化,例如在循环的终止条件下移动方法调用如strlen()。

一些编译器可以处理这个问题。 Visual C ++有一个“链接时代码生成”选项,可以有效地重新调用编译器来进行进一步的优化。 并且,在像strlen这样的函数的情况下,许多编译器会将其识别为内在函数。

有没有像我这样的优化,因为它们不能由编译器完成,必须手工完成? 另一方面,我是否应该避免使用任何技术,因为它们可能会干扰编译器自动优化代码的能力?

当你在这个低级别进行优化时,几乎没有可靠的经验法则。 编译器会有所不同。 测量您当前的解决方案,并确定它是否太慢。 如果是,请提出一个假设(例如,“如果我用查找表替换内部if语句怎么办?”)。 它可能有所帮助(“消除由于分支预测失败导致的停顿”)或者它可能会受到伤害(“查找访问模式会损害缓存一致性”)。 逐步实验和测量。

我经常克隆简单的实现,并使用#ifdef HAND_OPTIMIZED / #else / #endif在参考版本和调整版本之间切换。 它对以后的代码维护和验证很有用。 我提交了每个成功的实验以更改控制,并保留一个日志(电子表格),其中包含更改列表编号,运行时间以及优化中每个步骤的说明。 随着我对代码行为的了解越来越多,日志可以轻松地在另一个方向上进行备份和分支。

您需要一个框架来运行可重复的计时测试,并将结果与​​参考版本进行比较,以确保您不会无意中引入错误。

如果我正在研究这个问题,我会期待一个终结阶段,其中缓存局部性和向量运算等事情会发挥作用。

然而,在进入最后阶段之前,我希望找到一系列不同大小的问题与编译器级优化没什么关系,更多的是与奇怪的东西有关,这是永远无法猜到的,但一旦找到,很容易修复。 通常它们围绕类过度设计和数据结构问题展开。

这是这种过程的一个例子。

我发现带有迭代器的通用容器类,原则上编译器可以优化到最小周期,通常不会因某些不明原因而优化。 我还听到过SO的其他案例。

其他人说,在你做任何其他事情之前,请说明。 我同意这种方法,除了我认为有更好的方法,并在该链接中表明。 每当我发现自己询问是否某些特定事物(如STL)可能是一个问题时,我可能是对的 - 但是 - 我在猜测 性能调优的基本获胜理念是找出来 ,不要猜测。 很容易发现什么是花时间,所以不要猜。

而且我认为任何人都能给你的主要暗示是: 衡量衡量衡量 那和改进你的算法。
你使用某些语言功能的方式,编译器版本,std lib实现,平台,机器 - 所有这些都是他们在性能方面的作用,你没有提到很多这些,我们没有人有过你的确切设置。

关于替换std::vector :使用drop-in替换(例如, 这个 )并尝试一下。

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

我不能代表所有编译器,但我对GCC的经验表明,它不会大量优化缓存方面的代码。 我希望大多数现代编译器都适用。 优化(例如重新排序嵌套循环)肯定会影响性能。 如果您认为自己拥有可能导致许多缓存未命中的内存访问模式,那么调查此问题符合您的利益。

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

STL通常是最快的一般情况。 如果你有一个非常具体的案例,你可能会看到手工卷制的加速。 例如,std :: sort(通常是quicksort)是最快的常规排序,但如果您事先知道您的元素实际上已经被排序,那么插入排序可能是更好的选择。

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

这取决于您要进行静态分配的位置。 我尝试过这一行的一件事是静态在堆栈上分配大量内存,然后再重新使用。 结果? 堆内存大大加快。 仅仅因为一个项目在堆栈上并不能使访问更快 - 堆栈内存的速度也取决于缓存之类的东西。 静态分配的全局数组可能不比堆快。 我假设您已经尝试过仅保留上限的技术。 如果你有很多具有相同上限的向量,可以考虑通过包含数据成员的结构向量来改进缓存。

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

我个人通常在这种情况下通过引用传递结果。 它允许更多的重复使用。 按值传递大型数据结构并希望编译器使用RVO并不是一个好主意,只需自己手动使用RVO即可。

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

我发现他们并不是特别容易识别缓存。 问题是编译器不理解你的程序,也无法预测它的绝大多数状态,特别是如果你严重依赖堆。 如果您的编译器附带了一个分析器,例如Visual Studio的Profile Guided Optimization,那么这可以产生出色的加速。

鉴于该程序的科学性,浮点数被用于各处。 我的代码中的一个重要瓶颈曾经是从浮点到整数的转换:编译器会发出代码来保存当前的舍入模式,更改它,执行转换,然后恢复旧的舍入模式---即使程序中没有任何内容永远改变了舍入模式! 禁用此行为会大大加快我的代码速度。 我应该注意哪些与浮点相关的类似问题?

有不同的浮点模型 - Visual Studio提供了一个fp:快速编译器设置。 至于这样做的确切影响,我无法确定。 但是,您可以尝试更改编译器中的浮点精度或其他设置并检查结果。

C ++被单独编译和链接的一个结果是编译器无法进行看起来非常简单的优化,例如在循环的终止条件下移动方法调用如strlen()。 有没有像我这样的优化,因为它们不能由编译器完成,必须手工完成?

我从未遇到过这种情况。 但是,如果你真的担心这种情况,那么选择仍然是手动完成。 你可以尝试的一件事是在const引用上调用一个函数,向编译器建议值不会改变。

我想指出的另一件事是使用编译器的非标准扩展,例如Visual Studio提供的是__assume。 http://msdn.microsoft.com/en-us/library/1b3fsfxw(VS.80).aspx

还有多线程,我希望你已经走了这条路。 您可以尝试一些特定的选择,如另一个答案建议SSE。

编辑:我意识到我发布的很多建议直接引用了Visual Studio。 这是事实,但是,海湾合作委员会几乎肯定会为大多数人提供替代方案。 我最喜欢VS的个人经历。

STL优先级队列实现针对其功能进行了相当优化,但某些类型的堆具有可以提高某些算法性能的特殊属性。 斐波那契堆就是一个例子。 此外,如果您使用小密钥和大量卫星数据存储对象,如果您单独存储该数据,即使这意味着每个对象存储一个额外的指针,您将获得缓存性能的重大改进。

至于数组,我发现std :: vector甚至可以略微超出编译时常量数组。 也就是说,它的优化是一般性的,并且对算法访问模式的具体了解可能允许您进一步优化缓存局部性,对齐,着色等。如果您发现由于缓存效应导致性能显着下降超过某个阈值,请手在某些情况下,优化后的数组可能会将问题大小阈值移动两倍,但对于容易在缓存中容纳的小内环,或者超出任意大小的大型工作集,它不太可能产生巨大差异。 CPU缓存。 首先处理优先级队列。

动态内存分配的大部分开销相对于所分配的对象的大小是恒定的。 分配一个大对象并通过指针返回它不会像复制它那样受到太大伤害。 复制与动态分配的阈值在不同系统之间差异很大,但在芯片生成中应该相当一致。

当打开cpu特定的调优时,编译器可以识别缓存,但是他们不知道缓存的大小。 如果您正在优化缓存大小,则可能需要检测该缓存大小或让用户在运行时指定它,因为即使在同一代的处理器之间也会有所不同。

至于浮点,你绝对应该使用SSE。 这并不一定需要自己学习SSE,因为有许多高度优化的SSE代码库可以执行各种重要的科学计算操作。 如果您正在编译64位代码,编译器可能会自动发出一些SSE代码,因为SSE2是x86_64指令集的一部分。 SSE还可以节省一些x87浮点的开销,因为它不会在内部来回转换为80位值。 这些转换也可能是准确性问题的根源,因为根据它们的编译方式,您可以从同一组操作中获得不同的结果,因此最好摆脱它们。

例如,如果您处理大型矩阵,请考虑平铺循环以改善局部性。 这通常会带来显着的改善。 您可以使用VTune / PTU监视L2缓存未命中。

C ++被单独编译和链接的一个结果是编译器无法进行看起来非常简单的优化,例如在循环的终止条件下移动方法调用如strlen()。 有没有像我这样的优化,因为它们不能由编译器完成,必须手工完成?

在某些编译器上,这是不正确的。 编译器完全了解所有翻译单元(包括静态库)中的所有代码,并且可以像在单个翻译单元中一样优化代码。 我想到了一些支持此功能的功能:

  • Microsoft Visual C ++编译器
  • 英特尔C ++编译器
  • LLVC-GCC
  • 海湾合作委员会(我认为,不确定)

我很惊讶没人提到这两个:

  • 在支持链接时间优化上从4.5 链接时间优化 clang和g ++。 我听说在g ++案例中,启发式方法仍然相当不成熟,但是由于主要架构的布局,它应该会很快改进。

    优点包括目标文件级别的过程间优化,包括非常需要的内容,如虚拟调用的内部(虚拟化)

  • 项目内联可能看起来像是非常粗糙的方法,但是它非常粗糙,这使得它如此强大:这相当于将所有头文件和.cpp文件转储到一个非常大的.cpp文件中并编译它; 基本上它会给你带来与你1999年旅行中链接时间优化相同的好处。当然,如果你的项目真的很大,你仍然需要一台2010年的机器; 就像没有明天一样,这东西会占用你的内存。 但是,即使在这种情况下,您也可以将其拆分为多个不那么大的.cpp文件

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

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

这是一次对我有用的东西。 我不能说它会对你有用。 我的代码就行了

switch(num) {
   case 1: result = f1(param); break;
   case 2: result = f2(param); break;
   //...
}

然后,当我将其更改为时,我获得了严重的性能提升

// init:
funcs[N] = {f1, f2 /*...*/};
// later in the code:
result = (funcs[num])(param);

也许有人在这里可以解释后一版本更好的原因。 我想它与那里没有条件分支的事实有关。

我当前的项目是一个媒体服务器,具有多线程处理(C ++语言)。 这是一个时间关键的应用程序,一旦低性能功能可能导致媒体流的不良结果,如同步丢失,高延迟,巨大延迟等。

我通常用来授予可能的最佳性能的策略是最小化分配或管理内存,文件,套接字等资源的繁重操作系统调用的数量。

起初我写了自己的STL,网络和文件管理类。

我的所有容器类(“MySTL”)都管理自己的内存块,以避免多次alloc(新)/ free(删除)调用。 释放的对象排入内存块池,以便在需要时重用。 在这种方式,我提高性能,保护我的代码免受内存碎片。

需要访问性能较低的系统资源(如文件,数据库,脚本,网络写入)的代码部分我使用单独的线程。 但是每个单元没有一个线程(例如每个套接字没有一个线程),如果是这样,操作系统在管理大量线程时会失去性能。 因此,如果可能,您可以将相同类的对象分组以在单独的线程上进行处理。

例如,如果您必须将数据写入网络套接字,但套接字写入缓冲区已满,我将数据保存在sendqueue缓冲区(与所有套接字共享内存)中,并在一个单独的线程上发送套接字再次变得可写。 通过这种方式,您的主线程永远不会停止在阻塞状态下处理,等待操作系统释放特定资源。 释放的所有缓冲区都会在需要时保存并重复使用。

毕竟,欢迎使用配置文件工具查找程序瓶并显示应该改进哪些算法。

一旦我在linux机器上运行500天以上的服务器而没有重新启动,我就成功使用了该策略,每天有数千名用户登录。

[02:01] -alpha.ip.tv-正常运行时间:525天12小时43分7秒

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM