【问题标题】:Effective optimization strategies on modern C++ compilers现代 C++ 编译器的有效优化策略
【发布时间】:2011-02-25 07:50:34
【问题描述】:

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

众所周知,一些优化,例如循环展开,这些天由编译器比由程序员手动干预更有效地处理。哪些技术仍然值得?显然,我将通过分析器运行我尝试的所有操作,但是如果有关于哪些方法有效哪些方法无效的传统智慧,这将节省我大量的时间。

我知道优化非常依赖于编译器和架构。我正在使用面向 Core 2 Duo 的英特尔 C++ 编译器,但我也对什么对 gcc 或“任何现代编译器”有效。

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

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

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

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

【问题讨论】:

  • 你的问题是什么?
  • 为@Neil +1。然后,分析您的应用程序,找出瓶颈所在,并优化这些部分。
  • @Carl 当然。这就是我所做的,并且正在做的事情
  • 如果您阅读它,问题就在那里。 “哪些技术仍然值得?”
  • @ChrisW:或者您可以将其视为一个需要大量澄清的大问题,或者介于两者之间。 OP 显然对一个主题(如何在现代编译器上对 C++ 程序进行微优化)感兴趣,即使该问题不是规范形式。

标签: c++ optimization x86


【解决方案1】:

如果您正在执行繁重的浮点数学运算,那么您应该考虑使用 SSE 来对您的计算进行矢量化,前提是这可以很好地映射到您的问题。

Google SSE 内在函数了解更多信息。

【讨论】:

    【解决方案2】:
    1. 与动态分配相比,使用内存缓冲池可以带来巨大的性能优势。如果它们减少或防止长时间执行运行中的堆碎片,则更是如此。

    2. 注意数据位置。如果您有大量本地数据与全局数据混合,您可能会过度使用缓存机制。尽量让数据集靠近,以最大限度地利用缓存行有效性。

    3. 尽管编译器在循环方面做得很好,但在性能调整时我仍然会仔细检查它们。您可以发现产生数量级的架构缺陷,而编译器可能只会调整百分比。

    4. 如果单个优先级队列在其操作中使用大量时间,则创建一组表示优先级存储桶的队列可能会有所帮助。在这种情况下,以复杂性换取速度。

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

    祝你好运。

    【讨论】:

      【解决方案3】:

      Here 是一篇很好的论文。

      【讨论】:

        【解决方案4】:

        将 STL 容器/算法替换为手动容器/算法有什么好处?特别是,我的程序包含一个非常大的优先级队列(当前是一个 std::priority_queue),其操作占用了很多总时间。这是否值得研究,或者 STL 实现可能已经是最快的了?

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

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

        您提到您正在使用priority_queue,我认为这会为重新排列元素付出很多代价,尤其是在它们很大的情况下。您可以尝试切换底层容器(可能是deque 或专门的)。我几乎肯定会存储指针 - 再次确认,配置文件。

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

        同样,这可能会有所帮助,具体取决于用例。您可以避免堆分配,但前提是您不需要数组比堆栈长...

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

        您可以查看生成的程序集以查看是否应用了 RVO,但如果您返回指针或引用,则可以确定没有副本。这是否会有所帮助取决于您在做什么 - 例如。不能返回对临时对象的引用。你可以使用 arenas 来分配 并重用对象,因此不会支付大量的堆惩罚。

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

        我在这个领域看到了戏剧性的(非常戏剧性的)加速。我从中看到的改进比我后来从多线程代码中看到的要多。从那以后的五年里,情况可能发生了变化——只有一种方法可以确定——个人资料。

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

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

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

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

        【讨论】:

        • 不知道......你提出了很多非常有用的观点,特别是你的 cmets 关于你通过查看数据缓存一致性获得的加速 - 我将在我自己的代码中详细检查.
        【解决方案5】:

        这是我用过的一些东西:

        • 专门用于最内层循环边界的模板(使它们非常快)
        • 使用__restrict__ 关键字解决别名问题
        • 预先将向量保留为正常的默认值。
        • 避免使用地图(它可能真的很慢)
        • 向量追加/插入可能会非常慢。如果是这种情况,原始操作可能会使其更快
        • N 字节内存对齐(Intel 有编译指示对齐,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) {
        

        【讨论】:

        • 您关于std::map 的观点很有趣。是否有更好的标准库替代方案(hash_map,如果可用?)还是您建议自己滚动?
        • @user 好吧,在我的情况下,密钥是一对整数,并且大小是提前知道的。所以用二维指针数组替换了map,并将实际值保存在静态数组中。就我而言,这将性能提高了大约 20%。我也不能真正推荐,因为它不知道您的问题域。
        • hash_map 与 map 取决于键的数量。如果您有大量的键,那么散列是一个更好的主意,但如果它只有几个,那么映射会提供更好的性能。另外,我相信 hash_map 可能会提供更好的缓存一致性,就像普通配置文件一样。仅供参考,VS 和 GCC 都提供了 hash_map 扩展,我认为 VS 是用 std::map 拖放替换的。
        • 您能澄清一下“专门用于最内层循环边界的模板”是什么意思吗?
        【解决方案6】:

        我认为任何人都可以给你的主要提示是:measuremeasure 测量。这并改进您的算法。
        您使用某些语言功能的方式、编译器版本、std lib 实现、平台、机器 - 所有这些都在性能中发挥作用,您还没有提到其中的许多,而且我们没有人有过您的确切设置。

        关于替换 std::vector:使用直接替换(例如,this one)并尝试一下。

        【讨论】:

          【解决方案7】:

          替换 STL 有什么好处吗 手卷容器/算法 那些?特别是我的程序 包括一个非常大的优先级队列 (当前为 std::priority_queue) 谁的操纵需要很多 总时间。这值得吗 调查,或者是 STL 实施已经可能 最快?

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

          类似的思路,对于 std::vectors 其所需尺寸未知,但 有一个相当小的上限, 用它们代替它们是否有利可图 静态分配的数组?

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

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

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

          编译器倾向于如何感知缓存 是?例如,是否值得一看 重新排序嵌套循环?

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

          鉴于其科学性 程序,浮点数是 到处使用。一个重要的 我的代码中的瓶颈曾经是 从浮点数转换为 整数:编译器会发出代码 保存当前的舍入模式, 更改它,执行转换, 然后恢复旧的舍入模式--- 即使程序中没有任何内容 改变了舍入模式! 显着禁用此行为 加快了我的代码。有没有类似的 浮点相关的陷阱 I 应该知道吗?

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

          编译 C++ 的一个后果 并单独链接的是 编译器无法做到 看似很简单的优化, 比如 move 方法调用之类的 strlen() 退出终止 循环条件。有没有 像我这样的优化 应该注意,因为他们不能 由编译器完成并且必须是 手工完成?

          我从来没有遇到过这样的情况。但是,如果您真的对此感到担忧,那么仍然可以选择手动进行。您可以尝试的其中一件事是在 const 引用上调用函数,向编译器建议该值不会改变。

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

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

          编辑:我意识到我发布的很多建议都直接引用了 Visual Studio。确实如此,但是 GCC 几乎可以肯定地提供了其中大多数的替代方案。我只是对VS最有个人经验。

          【讨论】:

            【解决方案8】:

            查看优秀的Pitfalls of Object-Oriented Programming slides,了解有关本地化重构代码的一些信息。根据我的经验,获得更好的本地化几乎总是最大的胜利。

            一般流程:

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

            其他一些具体的事情:

            • Understand strict aliasing. 完成后,如果您的编译器有restrict,请使用它。 (在这里也检查一下disasm!)
            • 查看处理器和编译器上的不同浮点模式。如果您不需要非规范化范围,则选择没有此的模式可以获得更好的性能。 (根据您对舍入模式的讨论,听起来您已经在这方面做了一些事情。)
            • 绝对避免分配:尽可能在std::vector 上调用reserve,或者在编译时知道大小时使用std::array。李>
            • 使用内存池来增加局部性并减少分配/释放开销;还可以确保缓存行对齐并防止乒乓。
            • 使用帧分配器,如果您以可预测的模式分配事物,并且有能力一次性释放所有内容。
            • 注意不变量。您知道的不变的东西对于编译器可能不是,例如在循环中使用结构或类成员。我发现在这里养成正确习惯的最简单方法是给所有事物命名,并且更喜欢在循环之外命名事物。例如。 const int threshold = m_currentThreshold; 或者 Thing * const pThing = pStructHoldingThing-&gt;pThing; 幸运的是,您通常可以在反汇编视图中看到需要这种处理的东西。这也有助于以后进行调试(使 watch/locals 窗口在调试版本中表现得更好)!
            • 尽可能避免循环写入 - 先累积,然后写入,或将几个写入批处理。当然是 YMMV。

            WRT 你的std::priority_queue 问题:将东西插入向量(priority_queue 的默认后端)往往会移动很多元素。如果你可以分成几个阶段,在其中插入数据,然后对其进行排序,然后在排序后读取它,你可能会好很多。尽管您肯定会失去局部性,但您可能会发现像 std::map 或 std::set 这样更自序的结构值得开销——但这真的取决于您的使用模式。

            【讨论】:

            • 非常详细。您应该提到使用deque 作为优先级队列的基础。它在重新分配方面的表现优于vector,但在其他方面具有很多向量行为。
            • @Matthieu:我承认对deque 有一些偏见;我经常在模数较慢的硬件上工作。与vector 相比,迭代器很重。虽然deque 在序列的开头(除了与vector 一样的结尾)具有恒定的时间插入/删除元素,但我见过的实现在中间表现不佳(比vector 差)。如果您经常插入排序到前面的元素,这可能是值得的。 pop 似乎映射到 pop_front;双端队列可能会提高消费。可能应该针对特定案例进行配置!
            【解决方案9】:

            将 STL 容器/算法替换为手动容器/算法有什么好处?
            我只会将此视为最后的选择。 STL 容器和算法已经过全面测试。就开发时间而言,创建新的成本很高。

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

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

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

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

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

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

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

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

            循环中总是有倒计时优化。对于许多处理器,jump on register equals 0comparejump if less than 更有效。

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

            优化思路和概念

            1.计算机更喜欢执行顺序指令。
            分支让他们心烦意乱。一些现代处理器有足够的指令缓存来包含小循环的代码。当有疑问时,不要引起分支。

            2。消除要求
            更少的代码,更高的性能。

            3.在编码之前优化设计 很多时候,通过改变设计而不是改变设计的实现可以获得更多的性能。更少的设计促进更少的代码,产生更多的性能。

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

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

            6.考虑 I/O 优化 (也包括文件 I/O)。
            大多数 I/O 更喜欢较少的大数据块而不是许多小数据块。硬盘喜欢不停地旋转。较大的数据包比较小的数据包具有更少的开销。
            将数据格式化到缓冲区中,然后写入缓冲区。

            7.消除竞争
            摆脱与您的应用程序竞争处理器的任何程序和任务。诸如病毒扫描和播放音乐等任务。甚至 I/O 驱动程序也想要部分操作(这就是您想要减少 I/O 事务的数量的原因)。

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

            【讨论】:

              【解决方案10】:

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

              至于数组,我发现 std::vector 的性能甚至比编译时间常数数组稍微好一些。也就是说,它的优化是通用的,并且对算法访问模式的具体了解可以让您进一步优化缓存局部性、对齐、着色等。如果您发现由于缓存效应而导致性能显着下降超过某个阈值,请手- 优化的数组在某些情况下可能会将该问题大小阈值移动多达两倍,但对于容易放入缓存的小型内部循环或超过任何大小的大型工作集,它不太可能产生巨大的差异CPU 缓存。先处理优先级队列。

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

              当 cpu-specific 调优开启时,编译器对缓存非常敏感,但它们不知道缓存的大小。如果您正在针对缓存大小进行优化,您可能希望检测到它或让用户在运行时指定它,因为即使在同一代处理器之间也会有所不同。

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

              【讨论】:

                【解决方案11】:

                将 STL 容器/算法替换为手动容器/算法有什么好处?

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

                我见过的一个例外是将写入时复制的 std::string 替换为不需要线程同步的。

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

                不太可能。但是,如果您使用大量时间分配到某个大小,添加一个 reserve() 调用可能会有利可图。

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

                在使用容器时,我传递了输入迭代器和输出迭代器,这仍然很通用。

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

                不太好。是的。我发现错过的分支预测和不利于缓存的内存访问模式是性能的两个最大杀手(一旦你掌握了合理的算法)。许多旧代码使用“提前退出”测试来减少计算。但在现代处理器上,这通常比做数学运算而忽略结果更昂贵。

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

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

                单独编译和链接 C++ 的一个后果是编译器无法执行看似非常简单的优化,例如将 strlen() 等方法调用移出循环的终止条件。

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

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

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

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

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

                【讨论】:

                  【解决方案12】:

                  这是曾经对我有用的东西。我不能说它对你有用。我有代码在

                  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);
                  

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

                  【讨论】:

                  • 在汇编语言级别单步执行。这将告诉您编译器是否能够将 switch 语句转换为跳转表或只是一堆 if 语句。
                  【解决方案13】:

                  如果我正在处理这个问题,我希望缓存局部性和向量操作之类的东西会发挥作用。

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

                  Here's an example of this kind of process.

                  我发现带有迭代器的通用容器类,原则上编译器可以优化到最小周期,但由于一些模糊的原因,通常没有那么优化。我还听说过 SO 上发生这种情况的其他案例。

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

                  【讨论】:

                    【解决方案14】:

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

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

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

                    我所有的容器类(“MySTL”)都管理自己的内存块,以避免多次分配(新)/释放(删除)调用。释放的对象在内存块池中排队,以便在需要时重用。这样我就可以提高性能并保护我的代码免受内存碎片的影响。

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

                    例如,如果您必须将数据写入网络套接字,但套接字写入缓冲区已满,我将数据保存在发送队列缓冲区(与所有套接字一起共享内存)上,以便在单独的线程上发送一旦套接字再次变为可写。这样,您的主线程不应停止处理阻塞状态,等待操作系统释放特定资源。所有释放的缓冲区都会被保存并在需要时重复使用。

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

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

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

                    【讨论】:

                      【解决方案15】:

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

                      我不能代表所有编译器,但我使用 GCC 的经验表明,它不会针对缓存对代码进行大量优化。我希望这对于大多数现代编译器都是正确的。诸如重新排序嵌套循环之类的优化肯定会影响性能。如果您认为自己的内存访问模式可能会导致许多缓存未命中,那么调查此问题将符合您的利益。

                      【讨论】:

                        【解决方案16】:

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

                        【讨论】:

                          【解决方案17】:

                          关于 STL 容器。

                          这里的大多数人声称 STL 提供了最快的容器算法实现之一。我说的恰恰相反:对于最真实的场景,按原样使用的 STL 容器会产生真正的灾难性性能

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

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

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

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

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

                          从性能的角度来看,这证明了我更胜一筹。

                          但最近我发现我并不是唯一一个想过这个问题的人。 boost::intrusive 是容器库,其实现方式与我当时所做的类似。

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

                          【讨论】:

                            【解决方案18】:

                            单独编译和链接 C++ 的一个后果是编译器无法执行看似非常简单的优化,例如将 strlen() 等方法调用移出循环的终止条件。有没有像这样的优化我应该注意,因为它们不能由编译器完成,必须手动完成?

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

                            • Microsoft Visual C++ 编译器
                            • 英特尔 C++ 编译器
                            • LLVC-GCC
                            • GCC(我认为,不确定)

                            【讨论】:

                              【解决方案19】:

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

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

                                受益于目标文件级别的程序间优化,包括备受追捧的东西,例如虚拟调用的内联(去虚拟化)

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

                              【讨论】:

                                猜你喜欢
                                • 2013-03-11
                                • 2014-02-21
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                相关资源
                                最近更新 更多