【问题标题】:Ideas for good performance optimisations in C++C++ 中良好性能优化的想法
【发布时间】:2010-02-03 08:44:22
【问题描述】:

好的,过去三天我一直在查看分析器结果,这些结果是通过自动化套件运行大量测试用例生成的。这个想法是看看是否有任何好的优化通常可以提高性能。我将在这种情况下限定 good 如下;

  • 具有性能潜力 两者都非常的改进 结尾显着且可观察 用户级别,例如> 100% 改善 在表现不佳的领域。

  • 具有核心空间的潜力 减少使用量,例如> 减少 50% 在数据密集的领域。

  • 易于实施,最少 混淆代码,并且最小化 副作用。即的好处 大大实施优化 超过成本。

该应用程序是一个 3D 映射和建模包,界面中有大量图形,后端有几何处理。在确保大多数处理的最佳算法选择方面,我已经做了很多工作,在这个阶段,我正在寻找任何普遍适用的简单方法,以便在处理大型和复杂的数据集时获得额外的优势。到目前为止,我想出了以下内容;

  • 搜索时,保留最近找到的项目的缓冲区并首先检查。重复搜索的大量处理似乎在同一区域进行搜索。从迄今为止的答案来看,这似乎是 memoization

  • 的特定形式
  • 排序时,检查数据是否已经按排序顺序(特别是使用 qsort 的位置)

  • 将 GUI 和处理保持在单独的线程中(不符合易于实施的良好标准,但 IMO 仍然值得)

  • 如果你有局部类变量,在大量使用的成员函数中,这些变量具有显着的构造/销毁时间,请将它们设为私有类成员。尤其是动态数组和字符串,尤其是 MFC CArrays 和 CStrings。

  • 使用动态数组时,将初始大小设置为略高于典型使用量,并采用指数增长策略。

  • 在处理要存储在数组中的非常大的数据集时,请先调整数据大小以避免任何重新分配。

  • 避免在堆栈上创建临时对象副本的函数返回,而是使用引用参数,例如

    CString MyFunc(double x, double y)

效率低于

void  MyFunc(double x, double y, CString &Result)

实际上,在代码的任何性能关键区域都避免使用 CString 和大多数 MFC。 (编辑:这可能更普遍地被RVO 否定,尽管不适用于我的应用程序中的 CStrings)

这些项目似乎在我的上下文中运行良好,但有没有我遗漏的明显项目,或者还有其他关于优化的好资源?

编辑:根据提供的许多 cmets,显然需要进一步解释。虽然我完全意识到建议对特定代码段进行特定优化需要查看该代码,但过去几天花在分析分析器输出上的时间已经显示了优化候选者的某些模式。我也知道我自己对其他人在该领域所做的事情的无知,并且认为(至少对我而言)列举这些技术的价值,无论它们是否适用于我的情况。这个问题不是关于优化的危险,但对于任何初学者来说,我建议你在首先考虑之前首先建立一个需要优化的强烈要求。我自己的偏好是在设计阶段根据未来的性能要求进行大部分优化,但我也强烈主张进行分析以验证设计假设在实施中得到满足。我会要求人们将他们的答案限制在他们自己的积极优化经验上,而不是他们的信念上 是否应该首先考虑优化。

FWIW,在我的自动化套件中,让编译器优化代码与否之间的差异为 12%,这在最终用户级别是可以观察到的。

第二次编辑:我发现一些相关的帖子非常有益,尤其是 Mike Dunlavey 的 cmets 关于变得过于依赖分析器输出的文章。

Performance optimization strategies of last resort?

What can I use to profile C++ code in Linux?

【问题讨论】:

  • complete 引用(没有人使用过)是:“我们应该忘记小的效率,比如说大约 97% 的时间:过早优化是万恶之源”。无论如何,我有自己的一句话:“许多统计数据,比如大约 97%,都是当场制作的。”
  • 那么,分析器说大部分时间都花在了哪里?然后弄清楚如何“少做事”。
  • 我想说这个问题的答案完全取决于根据您的分析器的热点位置。没有它,我们只是在黑暗中拍摄——谁在乎你是否在热点之外用 CStrings 做了很多低效的事情呢?旁注:将 CStrings 作为返回值返回可能并不像您想象的那样低效——请参阅en.wikipedia.org/wiki/Return_value_optimization
  • 如果您还没有尽量减少内存访问,请为您的数据找到最紧凑的形式。如果你有很好的数据抽象,这应该不会改变你的代码。
  • 这不是一个有意义的问题。优化取决于上下文。向我们展示一些代码,我们可以建议如何优化它。如果没有代码,您的问题将毫无意义,尤其是当您限制答案说它们必须至少如此有效时。事实上,这将是我的优化建议:查看您的代码并修复缓慢的位

标签: c++ optimization


【解决方案1】:

请不要扔掉任何旧的 “优化是万恶之源” 东西,和这个完全无关 问题

是的,然后你有类似的东西:

排序时,检查数据是否已经排序

让我想知道您是否使用了有效的算法。这就是“过早优化是万恶之源”的基本前提。

将被否决。

真的吗?这语气不好。国际海事组织。 YMMV。

同样,我对 让编译器优化的乐趣 对我来说,或者全部扔内联 在那个地方。 FWIW,区别 在让编译器优化之间 代码是否出现在 12% 在我的自动化套件中,这是 最终用户可观察到的边界线 级别。

再加上您手动进行的任何优化,您仍然希望对编译器进行优化。

除此之外,由于您没有提供有关瓶颈所在位置的具体见解,因此即使不是不可能,也很难提供任何指示。我敢猜测你至少:

  • 玩转自定义分配器
  • 如果您的目标机器是向量机,请探索使用向量指令的可能性

编辑:既然您说您不了解 RVO:请尝试阅读移动语义,尤其是这个库:来自 Adob​​e 的 move library。我猜 Boost 也会有类似的东西。

编辑#2:还有两件事:

  • 定点算术可能是一个很好的查找方法
  • 尽可能多地使用查找表

【讨论】:

  • +1“想知道你是否使用了一种有效的算法”,“如果不是不可能,也很难提供任何指针”:听听。
  • 显然是的——你的意思是我引用的陈述具有讽刺意味吗...?
  • @Shane MacLaughlin:你可能是问这个问题的人,但阅读答案会更多。因此,在我们引导毫无戒心的读者尝试真正、真正应该是最后手段的东西之前,它总是有帮助的。
  • @Shane:我使用了数十种(如果不是数百种)我认为有益的“优化技术”。问题是我完全不知道其中哪些适用于,因为你没有告诉我们任何关于你的代码的事情。心理调试很难,但心理优化是完全不可能的。在某些情况下,几乎每一次优化都会变成悲观。它 120% 取决于应用它的上下文,而您没有给我们任何上下文。
  • @dirkgently 和@jalf:编辑了问题的语气,使其看起来不那么具有煽动性,警告新手,并更好地解释我的目标。顺便说一句,你有没有想过有多少推出 Knuth/Hoare 名言的人真正读过 Knuth 或 Hoare?
【解决方案2】:

CString MyFunc(double x, double y)

效率低于

void MyFunc(double x, double y, CString &Result)

如果 MyFunc 写得很干净,它们应该大致相同。编译器应该能够利用NRVO。听起来您已经分析并发现它不是 - 我只是说它可能更符合您的标准,例如“对代码的最小混淆”,重新排列函数本身以允许 NRVO 发生。

还有几件事要尝试:

  1. memoization(类似于您的缓存 重复搜索,但更多 专注于树/图形分辨率, 如果有的话)。
  2. 如果您不需要额外的,请使用浮点数而不是双精度数 精度(或者甚至是整数,如果你 可以)。
  3. 使用类型来标记假设(您提到了排序数组 - 另一个常见的是小写的 字符串)。创建派生或 包装类型(例如Sorted<T>) 使这些假设明确。那 如果你有一种方法 Sorted<vector<T> >,例如, 你给它一个排序的向量,它 直接通过 - 但如果你 给它一个vector<T>,它必须 构造了一个Sorted<vector<T> >,在 它将对其进行排序。你可以 手动断言它已排序 使用替代构造函数, 但它使携带更容易 你周围的假设,也许 抓住你可能有的地方 否则错过。
  4. 不要放弃内联等。确保您完全了解何时 他们应该提供帮助,当他们 应该阻碍。在某些情况下,他们 真的可以有所作为 - 但是 如果你只是处理可能不会 随意。
  5. 您可能会从flyweightpooled object allocators 中受益。
  6. 穿线时, 尽量减少任何互动,所以 你可以减少代码量 这需要锁定。经常服用 甚至相当昂贵的副本 对象可以减少开销 比互斥锁。明显利用 尽可能多的原子类型。

【讨论】:

  • 谢谢菲尔,我正在寻找的东西类型。只需阅读有关记忆化en.wikipedia.org/wiki/Memoization 的维基百科文章,还有其他好的参考资料吗?
  • 它更常与函数式编程相关联,因此您可能会在 FP 文献中找到更多参考资料。恐怕我手头没有现成的裁判。不过,维基百科的文章应该足以给你这个想法。
【解决方案3】:

我认为您的要求几乎是相互排斥的,除非存在某种明显的缺陷(所有分析都非常适合查找)。

真正改变性能的东西需要付出很多努力,而你的基本数据结构是最重要的。减少内存碎片,对齐内存管理,SIMD,尽可能小的数据结构并尽可能多地分配在一个块中,多线程,减少模板的代码大小,将参数重新声明为局部变量,以便优化器知道它们是相同的进行优化。如果不付出高昂的代价,这些都无法在最后完成。

很多时候,您甚至无法轻松衡量真正影响性能的因素,因为它们只会随着程序运行或代码大小的增长而变得昂贵。

【讨论】:

  • 写你的最后一段@Charles,我不明白为什么不能在程序运行或改变问题大小时检测代码来衡量性能。我一直这样做。
  • 问题是当涉及到内存碎片和缓存问题时,你不能轻易说出你有什么影响。看起来一棵树的实现要好得多,但是一旦程序运行一段时间,数组中更简单的线性八叉树可能会比实际使用的 BSP 快很多倍。
  • 您当然有权要求要求经常发生冲突,但尽管它们可能看起来是相互排斥的,但它们通常并非如此,就像搜索缓冲的情况一样,我刚刚从另一个答案中发现记忆化的具体案例 (en.wikipedia.org/wiki/Memoization) 我也同意最好的优化将发生在设计阶段,但也发现分析很有价值,如果只是为了证明设计假设。我必须在 C++ 中查找更多关于 SIMD 和示例应用程序的信息。有什么好的参考吗?
  • 关于您的最后一段,分析是与涵盖非常多典型用例的自动化测试套件一起进行的。 (我正常的五小时回归测试要运行四天!)
  • 您的编译器文档和 intel.com 是唯一真正有用的东西,真的。如果您使用 SIMD,那么您也可以使所有内存分配对齐,无论是通过覆盖内存分配还是编译器对齐声明。如果你不这样做,你必须做这种愚蠢的洗牌废话,这不仅是一个很大的痛苦,而且会消除 SIMD 的很多好处。一般来说,对齐内存可以将任意代码的执行速度提高约 30%。
【解决方案4】:

我害怕庞大而复杂的数据结构。我喜欢大而简单的数据结构;特别是数组。当我有大而简单的数据结构时,我可以尝试通过内存访问做一些聪明的事情,以真正优化我对缓存的使用;特别是内存平铺。不确定这对您是否有用,但总的来说,鉴于您的一组要求和对代码的现有理解,我会寻找优化从 RAM 到 CPU 获取数据的方法。

而且,我当然会并行处理这一切。除非您拥有多台计算机,否则这是不可能的。哦,备忘录就在里面,这些天我们都有了!!

祝你好运,让我们知道你的进展如何。我读了很多关于 SO 的废话,关于什么应该是优化这段或那段代码的最佳方法,很少有确凿的证据表明有人曾经像你所做的那样衡量任何事情。

哎呀,我非常喜欢你的问题,我赞成它。

问候

标记

【讨论】:

  • 我真的不明白你的最后一段。他说他测量了他的代码,然后询问一般代码的优化技巧,因此绝对没有使用他的测量。您在没有确凿证据支持的情况下批评优化建议,但是用确凿证据不可能回答这个问题,因为我们没有看到任何代码行。无论我们建议什么,我们都无法确定它是否真的对他的案子有帮助。
  • ++ 对于您担心大型复杂数据结构的评论。以我的经验,大多数问题都是由于设计人员对数据感到满意 - 以可爱的类层次结构为荣,充满了冗余数据的通知式维护。
【解决方案5】:

从优化自己的时间开始。不要打扰盲目地尝试列出和/或应用优化。不要因为不信任编译器执行 NRVO 而浪费时间将返回值转换为引用参数。

不要浪费时间手动将函数标记为inline。不要浪费时间尝试收集某种通用的“优化词典”。

97% 的代码在性能方面无所谓。如果您尝试应用优化而不管它们做什么以及它们在哪里有用,您将浪费 97% 的时间。所有这些时间可以用于优化真正重要的 3% 的代码。 (顺便说一句,这就是 Knuth 引用“万恶之源”的真正含义。并不是说不应该执行优化,而是除非你有一段你已经知道是热点的特定代码,否则你的优化 1)过早,2)无效)

所以首先优化建议:关闭此问题并寻求帮助,以优化您应用中重要的特定代码。通过询问一般优化技巧,您不会学到任何关于优化 3% 的应用的有用信息。

第二个优化建议(假设您现在正在查看一个特定的热点,并且在选择正确的算法、并行化和其他高级优化方面已经尽了最大努力): 查看编译器的程序集输出。

第三个优化建议:了解您正在运行的系统。构建代码以利用空间和时间局部性,以最大限度地减少缓存未命中。只需将 2D 数组的遍历从列优先顺序切换到行优先顺序,就可以轻松地使性能翻倍。请记住,编译器和 CPU 都会对指令进行重新排序以提高吞吐量,但分支限制了它们这样做的能力,因此请尝试构建代码以获得相当大的基本块,而不会跳入或跳出它们。如果您在支持 SIMD 指令的 CPU 上运行,请考虑是否可以有效地使用它们。如果您必须真正深入研究指令级优化,请确保您掌握所使用指令的延迟。对于浮点重代码,请记住,FP 指令通常不会被编译器或 CPU 自动重新排序。因为 FP 操作有相当长的延迟,依赖链可能是一个真正的性能杀手。手动分解它们可以显着加快您的代码速度。同样,避免内存依赖。首先写入,然后读取地址的代码会很慢。如果无法优化一个或两个内存访问,那么您必须等待写入完成(否则可能会在后台发生而不会停止 CPU),然后再开始读取。将所有经常使用的数据放在临时文件中,以避免混叠。

至于您问的优化“大型复杂数据集”吗?我完全没有头绪。复杂数据集的问题在于它们几乎没有共同点。没有通用方法可以优化它们。

最后一个建议:听起来您并不是真的在编写 C++。您正在谈论手动实现动态数组,您正在谈论 reallocs 和 MFC 类。听起来你在写“C with classes”。使用标准库。std::vector 已经存在。 std::string 也是如此。 C++ 标准库具有非常高效的优点。对于 MFC 类则不能这样说。

【讨论】:

  • 恕我直言,我暂时不相信没有广泛适用的优化技术。当然,这是研究算法和复杂性理论的基础。其次,就无法支持的概括而言,是什么让您能够评论您未见过的任何人 97% 的代码。最后,您是否真的用类来敲 C++,同时建议我恢复到 assembler 。当然,我可以将无数遗留代码从 MFC 重构为标准 C++,但我需要更具体的证据来证明我会看到这项投资的回报。
【解决方案6】:

我很惊讶似乎没有人对某一点做出回应。在您的问题中,您提到使用qsort()。在相当多的情况下,您可以通过使用std::sort() 而不是qsort() 来获得相当大的速度提升。改进的大小通常取决于您的比较函数的复杂程度,但我发现 2:1 改进相当典型,5:1 甚至 10:1 有时是可能的。

【讨论】:

    【解决方案7】:

    我很同情你的立场,尤其是

    我一直坐在探查器前面 过去三天的结果

    我假设您在智能地编写应用程序方面做得很好。现在,这是我的建议:

    • 不要寻找“任何通常可以提高性能的良好优化”,而您只是靠猜测来尝试。当您知道什么需要时间时,这些会很明显。

    • 确实有一个更好的方法来找出需要时间的东西,而不是盯着分析器输出。
      This is how I do it.

    我的经验表明你有更多的加速空间This is my canonical example.

    ...祝你好运。让我们知道结果如何。

    【讨论】:

    • 很棒的链接帖子。 wrt您的第一点,我不打算通过猜测来尝试优化。我更感兴趣的是看看其他人做了什么,以及在什么情况下,以获得一些未来优化的想法。您的示例与我的经验非常接近,尤其是在收益递减方面,即 82 秒-> 72 秒-> 76 秒-> 24 秒-> 18 秒-> 16 秒-> 停止。请注意中间的错误优化,我发现这种情况有时会发生。要从 16 秒变为 3-4 秒,需要进行重大的重新思考/重新设计,但目前 5 倍还是不错的。
    • @Shane:听起来你做对了。我所说的“猜测”是其他人也说过的一个主题。许多人只是盲目地尝试事情,并想知道为什么他们没有多大帮助。我喜欢你提出的整个问题,因为我们听到了一些真正的专业知识。
    【解决方案8】:

    +1 好问题。

    咆哮 我认为你要求人们不要为过早的优化而喋喋不休是对的——这个陈旧的栗子经常被推出来作为懒惰设计或草率实施的借口。存在无缘无故的优化不足之类的事情,通常是由于算法选择不当造成的。 结束咆哮

    我不知道这 97% 的东西是从哪里来的。我被教导了 80/20 规则——20% 的代码在 80% 的时间内运行,有趣的是,这似乎也适用于软件以外的其他事物。总之……

    首先停靠港始终是算法 - 确保您使用的是有效的算法。一个未优化的好算法几乎总是会击败一个高度优化的坏算法。我怀疑你已经知道了。

    我发现的一些一般优化可能很有用:

    1. 线性搜索通常会产生相当多的开销。通常这些可以用二进制搜索一个排序的集合来代替。

    2. 分类时需要小心。尽管快速排序是一种很好的通用排序算法,但有时当您的集合已经排序或部分排序时,冒泡排序或其他类型的排序会更快。

    3. 插入已排序的集合 - 可以使用二进制搜索来找到正确的位置,而不是“坚持到底,然后排序”的幼稚(尽管通常足够好)实现。

    4. 将键与值分开应该有助于通过更有效地利用缓存来更快地搜索键。

    5. 当两个线程作为供应商/消费者交互时,使用双缓冲区并交换,以最大限度地减少需要锁定缓冲区的时间。线程处理单独的数据副本,然后再修复,而不是让它们相互锁定,通常会更快。

    6. 不要依赖编译器来优化紧密循环内的构造函数和析构函数 - 要么将对象移出循环并重用,要么确认编译器已按照您的意愿对其进行了优化。

      李>

    【讨论】:

      【解决方案9】:

      如果您在采样分析器下运行代码并发现有任何计算密集型例程需要花费大量时间,那么可以考虑进行许多微优化,包括 SIMD 的使用,如果您能够限制您对特定 CPU 的支持。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-24
        • 2012-08-14
        相关资源
        最近更新 更多