【问题标题】:Performance of breaking apart one loop into two loops将一个循环分成两个循环的性能
【发布时间】:2012-03-09 13:23:04
【问题描述】:

美好的一天,

假设您有一个简单的 for 循环,如下所示...

for(int i=0;i<10;i++)
{
    //statement 1
    //statement 2
}

假设语句 1 和语句 2 是 O(1)。除了“启动”另一个循环的小开销之外,将 for 循环分解为两个(不是嵌套的,而是顺序的)循环是否同样快?比如……

for(int i=0;i<10;i++)
{
    //statement 1
}
for(int i=0;i<10;i++)
{
    //statement 2
}

为什么我会问这样一个愚蠢的问题,因为我有一个碰撞检测系统 (CDS),它必须遍历所有对象。我想“划分”我的 CDS 系统的功能,这样我就可以简单地调用

cds.update(objectlist);

而不必破坏我的 cds 系统。 (不要太担心我的 CDS 实现......我想我知道我在做什么,我只是不知道如何解释它,我真正需要知道的是我是否会因为循环而受到巨大的性能影响通过我所有的对象再次

【问题讨论】:

    标签: c++ loops big-o


    【解决方案1】:

    就算法复杂度而言,拆分循环没有区别。

    就现实世界的性能而言,拆分循环可能会提高性能、降低性能或没有任何影响 - 这取决于操作系统、硬件,当然还有 statement 1statement 2 是什么。

    【讨论】:

      【解决方案2】:

      这取决于您的应用程序。

      可能的缺点(分裂):

      • 您的数据不适合 L1 数据缓存,因此您为第一个循环加载一次,然后为第二个循环重新加载它

      可能的收益(分裂的):

      • 您的循环包含许多变量,拆分有助于减少寄存器/堆栈压力,优化器将其转化为更好的机器代码
      • 您使用的函数会丢弃 L1 指令缓存,以便在每次迭代时加载缓存,而通过拆分,您可以在每次循环的第一次迭代时(仅)加载一次

      这些列表当然并不全面,但您已经可以感觉到 codedata 之间存在张力。因此,当我们都不知道时,我们很难做出有根据的猜测。

      有疑问:个人资料。使用 callgrind,检查每种情况下的缓存未命中,检查执行的指令数。衡量花费的时间。

      【讨论】:

        【解决方案3】:

        您需要支付两个循环:

        • 增加了生成的代码大小
        • 分支预测数的 2 倍
        • 根据语句 1 和 2 的数据布局,您可以将数据重新加载到缓存中。

        最后一点可能会对任一方向产生巨大影响。您应该像使用任何性能优化一样进行测量。

        【讨论】:

        • 您的第三点可能是最重要的。这将归结为您是否适合一级 CPU 缓存。如果两者结合所有数据适合缓存拆分可能不会有帮助,但如果缓存太大且拆分足够小,则收益可能很大。
        【解决方案4】:

        就 big-o 复杂度而言,如果 1 循环为 O(n),则这没有区别,那么 2 循环解决方案也是如此。
        至于微优化,很难说。循环的成本相当小,我们不知道访问对象的成本是多少(如果它们在一个向量中,那么它也应该相当小),但是有很多需要考虑才能给出一个有用的回答。

        【讨论】:

          【解决方案5】:

          如上所述,复杂性仍然存在。

          但在现实世界中,我们无法预测哪个版本运行得更快。以下是发挥作用的重要因素:

          • 数据缓存
          • 指令缓存
          • 推测执行
          • 分支预测
          • 分支目标缓冲区
          • CPU 上可用寄存器的数量
          • 缓存大小

          (注意:在所有这些中,都有误判的达摩克利斯之剑;所有这些都可以在维基百科和谷歌上搜索)

          尤其是最后一个因素,有时无法为性能依赖于特定缓存大小的代码编译一个真正的代码。一些应用程序在具有大缓存的 CPU 上运行速度更快,而在小缓存上运行速度较慢,而对于其他一些应用程序则相反。

          解决方案:

          • 让您的编译器完成循环转换的工作。现代 g++ 在该学科中相当出色。 g++ 擅长的另一门学科是自动矢量化。请注意,编译器比几乎所有人都更了解计算机架构。
          • 发送不同的二进制文件和调度程序。
          • 使用适应目标缓存的cache-oblivious data structures/layouts and algorithms

          努力开发适应目标的软件总是一个好主意,最好不要牺牲代码质量。在进行手动优化之前,无论是微观还是宏观,测量现实世界的运行情况,然后才进行优化。

          文学: * Agner Fog's Guides * Intel's Guides

          【讨论】:

            【解决方案6】:

            您说得对,创建第二个循环会产生一些性能开销。因此,它不能“同样快”;因为这个开销虽然很小,但仍然是开销。

            我不会聪明地谈论应该如何构建碰撞系统,但如果您想优化性能,最好避免构建不必要的控制结构,前提是您可以在不费吹灰之力的情况下进行管理。

            请记住,过早优化是您能做的最糟糕的事情之一。在我看来,当您遇到性能问题时,请担心优化。

            【讨论】:

            • 正如 stefaanv 所说,第二次循环遍历所有对象的成本取决于您提供的信息。
            • 我还要注意,您发布的两个控制结构解决了不同的问题,因此在性能方面不容易进行比较。
            • 在不了解更多细节和没有实际测量的情况下,无法说哪个版本更快。缓存,数据和指令,以及分支预测(和 - 表)和推测执行为当今的优化增加了很多复杂性。好点虽然在过早的优化。首先在现实世界中进行测量,然后进行优化。
            猜你喜欢
            • 2019-03-08
            • 2021-12-13
            • 2013-10-03
            • 1970-01-01
            • 2020-10-17
            • 1970-01-01
            • 1970-01-01
            • 2014-07-29
            • 2021-12-20
            相关资源
            最近更新 更多