【问题标题】:Algorithm to optimize nested loops优化嵌套循环的算法
【发布时间】:2011-09-17 21:22:03
【问题描述】:

是否有可用于优化以下性能的算法?

for (i = 0; i < LIMIT; i++) {
  for (j = 0; j < LIMIT; j++) {
   // do something with i and j
  }
 }
  • i 和 j 都从 0 开始
  • 两个循环都以相同的条件结束
  • i 和 j 都以相同的速率递增

这可以以某种方式在 1 个循环中完成吗?

【问题讨论】:

  • 为什么要优化这个?循环按原样完美无缺。
  • i 和 j 都以相同的速率递增 - 不。 j 增加 LIMIT 倍的频率,如果以相同的增量。

标签: algorithm for-loop


【解决方案1】:

可以用一个循环来写这个,但我强烈建议不要这样做。双 for 循环是一种成熟的习惯用法,程序员知道如何阅读,如果将两个循环折叠成一个循环,就会牺牲可读性。此外,尚不清楚这是否真的会使代码运行得更快,因为编译器已经非常擅长优化循环。将两个循环合并为一个循环需要在每一步进行一些额外的数学运算,这几乎肯定比两个独立的循环要慢。

也就是说,如果您确实想将其编写为单个循环,则一个想法是考虑 迭代空间,即您迭代的一组对。现在,看起来像这样:

(0, 0)   (0, 1),   (0, 2), ...,   (0, N-1)
(1, 0)   (1, 1),   (1, 2), ...,   (1, N-1)
                ...          
(N-1, 0) (N-1, 1), (N-1, 2), ..., (N-1, N-1)

我们的想法是尝试按(0, 0), (0, 1), ..., (0, N-1), (1, 0), (1, 1), ..., (1, N-1), ..., (N-1, 0), (N-1, 1), ..., (N-1, N-1) 的顺序访问所有这些对。为此,请注意,每次增加 i 时,我们都会跳过 N 元素,而当我们增加 j 时,我们只会跳过一个元素。因此,循环的迭代(i, j) 将映射到线性循环排序中的位置i * N + j。这意味着在迭代i * N + j 时,我们想要访问(i, j)。为此,我们可以使用一些简单的算法从索引中恢复i 和j。如果k是当前循环计数器,我们要访问

i = k / N   (integer division)
j = k % N

因此循环可以写成

for (int k = 0; k < N * N; ++k) {
    int i = k / N;
    int j = k % N;
}

但是,您必须小心这一点,因为N * N 可能不适合整数,因此可能会溢出。在这种情况下,您可能希望使用双 for 循环。此外,引入额外的除法和模数将使该代码运行(可能)比双 for 循环慢得多。最后,此代码比原始代码更难阅读,您需要确保提供积极的 cmets 来描述您在这里所做的事情。同样,我强烈建议您不要这样做,除非您有充分的理由怀疑标准双 for 循环存在问题。

(有趣的是,这里使用的技巧也可以用于使用一维数组来表示多维数组。逻辑是相同的——你有一个二维结构,你想用一维结构来表示。 )

希望这会有所帮助!

【讨论】:

  • 嗯,使用后增量和前增量有帮助吗? IE。元素[i++]=元素[++j]?
  • 值得注意的是,模数可以比位掩码慢两个数量级。 (如果编译器可以保证 N 是 2 的幂,那么它可能会在后台执行此操作。)除法类似地可以用位移替换,尽管它不会快得多(如果有的话)。
  • 关于使用嵌套 for 循环为其他开发人员提供更好的可读性和上下文的有趣评论。我想在优化和可读性之间总是有一个权衡,有时,也许对于廉价的操作(小数据集等),嵌套的 for 循环就可以了。一直以来,我一直在想,如果我可以优化的话,使用它们是不好的做法……
【解决方案2】:

没有办法显着优化循环本身。但是,当您考虑“用 i 和 j 做某事”的细节时,i 或 j 是外循环会产生很大的不同。例如,一个顺序可能会导致内存或磁盘中的大量跳转,而另一种顺序会导致顺序访问,或者几乎如此。

此外,您有时可以通过将不依赖于内部索引的计算从内部循环移动到外部循环来优化双循环,可能使用临时变量。智能编译器可能会在一定程度上对此进行优化,但它们并不完美。

【讨论】:

    【解决方案3】:

    您无法提高循环的大 O 性能。但是,有一些算法相关的方法可以通过利用缓存来改进被大 O 隐藏的常数因子。

    这里是一个改进的矩阵转置算法的例子:A Cache Efficient Matrix Transpose Program?

    然而,这里的共同主题是我们实际上引入了更多循环,而不是更少。

    【讨论】:

      【解决方案4】:

      如果您必须不惜一切代价加快 for 循环,请查看是否可以找到并行化或矢量化编译器并根据需要对其进行修改以利用它,或者找到使用某些构建块库的方法.参见例如http://en.wikipedia.org/wiki/Intel_C%2B%2B_Compiler 和 http://en.wikipedia.org/wiki/Math_Kernel_Library。

      (或者找到一个更好的算法 - 通常会给你类似以下的东西:

      for (i = 0; i < LIMIT; i++) {
        // Do something clever with i 
        // that does not depend on j
        for (j = 0; j < LIMIT; j++) {
          // do something fast with i and j
          // and the results of the clever stuff
          // outside the loop over j
        }
      }
      

      )

      【讨论】:

        【解决方案5】:

        这取决于您是否需要在内循环中同时使用 i 和 j,例如有时您可以像这样展平这样的循环:

        for (k = 0; k < LIMIT * LIMIT; ++k)
        {
            // do something with k
        }
        

        但对于除了最微不足道的内部循环之外的所有循环,它可能对性能没有明显的影响。

        您实际上想解决什么具体问题?

        【讨论】:

          【解决方案6】:

          我前段时间遇到了同样的问题...

          您对此有何看法?单个 while 循环(在您的示例中 i 是外部 for 循环的索引):

          i = 0; j = 0;
          while (i<M) {
            // Do something with i and j
            if (j<N-1) {
              j++;
            } else {
              j=0;
              i++;
            }
          }
          

          【讨论】:

          • 可能会很长一段时间,但这不是和双 for 循环一样复杂吗?我想你会在这里迭代 N-1 次 forearch i。 OP说他的长度相同,所以它是j&lt;M,所以你实际上会迭代M*M次,我相信这是O(n2)?如果我错了,请纠正我,我正在努力理解算法:p
          猜你喜欢
          • 2021-01-21
          • 1970-01-01
          • 1970-01-01
          • 2012-01-27
          • 2015-05-29
          • 2020-05-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多