嵌套循环效率c

时间:2016-12-08 11:30:17

标签: c optimization nested-loops

哪种方式更好,为什么?

案例1:

for(i=0; i< 100; i++)
 for(j=0; j< 10; j++)
  printf("Hello");

案例2:

for(i=0; i<10; i++)
 for(j=0; j< 100; j++)
  printf("Hello");

2 个答案:

答案 0 :(得分:2)

一般来说,两种形式都不是更好或更快。编译器甚至可以将两个版本优化为仅使用一个循环的代码,在这种情况下,两个版本将产生相同的机器代码。

编辑

我使用gcc -O3编译了两个版本,两个版本都提供了相同的(虽然含糊不清)机器代码(x86):

0x00402CF0  push   %rsi
0x00402CF1  push   %rbx
0x00402CF2  sub    $0x28,%rsp
0x00402CF6  mov    $0xa,%esi
0x00402CFB  callq  0x4022f0 <__main>
0x00402D00  mov    $0x64,%ebx
0x00402D05  lea    0x12f4(%rip),%rcx        # 0x404000
0x00402D0C  callq  0x402ba8 <printf>
0x00402D11  sub    $0x1,%ebx
0x00402D14  jne    0x402d05 <main+21>
0x00402D16  sub    $0x1,%esi
0x00402D19  jne    0x402d00 <main+16>
0x00402D1B  xor    %eax,%eax
0x00402D1D  add    $0x28,%rsp
0x00402D21  pop    %rbx
0x00402D22  pop    %rsi
0x00402D23  retq

用于基准测试的代码,gcc -std=c11 -pedantic-errors -Wall -Wextra -O3

#include <stdio.h> 

#define I 100  // only change these 2 constants between builds
#define J 10

int main (void)
{
  for(int i=0; i<I; i++)
    for(int j=0; j<J; j++)
      printf("Hello");

  return 0;
} 

效率问题只会在你做这样的事情时出现:

// BAD, enforces poor cache memory utilization
for(i=0; i<n; i++)
  for(j=0; j<n; j++)
    array[j][i] = something;

// BAD, enforces poor cache memory utilization
for(j=0; j<n; j++)
  for(i=0; i<n; i++)
    array[i][j] = something;

// GOOD, optimized for data cache
for(i=0; i<n; i++)
  for(j=0; j<n; j++)
    array[i][j] = something;

答案 1 :(得分:2)

假设我们正在谈论在启用优化的情况下编译代码的情况,因为谈论&#34;效率&#34;是无意义的。或&#34;表现&#34;何时禁用优化...

这些编译为相同的目标代码。所有循环边界都是编译时常量,因此编译器理论上可以确定循环体中的代码执行次数,将所有内容折叠到单个循环中,然后发出该代码。如果它想要(并且它不会,因为这非常愚蠢并且不能提供显着的速度提升),它可能会向printf函数发出10,000次连续调用。这只是基本的循环展开,现在几乎所有优化编译器都会这样做。

在现实世界中,编译器不会执行魔术(并且它们通常不会优化以优化哑代码或识别其模式),因此这些代码段实际上会编译为略有不同的目标代码。


查看GCC的输出,它将标准的窥孔优化应用于循环,但它并没有合并它们。它也完全按照你编写的方式完成循环。 Test1 Test2Test1的代码基本相同,除了Test2在外循环中大约100次,在内循环中大约10次,虽然Test1恰恰相反。这只是将不同常数移入寄存器的问题。

MSVC在生成代码时遵循相同的策略。它对循环结构的基本模式优化与GCC相比略有不同,但代码在道德上是等效的。 Test2printf之间的唯一区别在于外循环是从0旋转到100还是从0旋转到10。

性能怎么样?那么,回答这个问题的唯一正确方法就是编译样本并检查。事实上,这是您达到绩效问题客观答案的唯一方式。但是,如果你尝试这样做,你会立刻遇到问题:循环中的Test2函数将大量占据其他任何东西占用的时间,从而导致你的基准测试结果吵闹无意义你需要弄清楚在循环内部做的其他事情,这对你试图测量的时间没有那么大的影响,并且它必须是副作用的东西,防止编译器对其进行简单的优化。这就是为什么这样的微基准标记非常难以正确完成的原因。它们也不是特别有趣;您应该进行基准测试的是真实代码。这不是真正的代码。因此,我甚至不愿意尝试从中获取有意义的基准数据。

我唯一允许自己做的就是关于为这两个函数生成的代码的概念性能影响。我会猜测使得内循环(Test1)的较大循环更快 。为什么?好吧,因为一旦代码被加载到指令缓存中,它就会以快速顺序执行100次,分支预测器几乎在所有情况下都成功地预测了分支的目标。这与紧密循环一样高效。在另一种情况下,您只能在这些最佳条件下进行10次迭代,然后才能重新启动,这样就有可能从指令缓存中驱逐代码。您必须测试和/或真正研究代码的细节,看看它是否真的有可能,因为它取决于代码的确切大小以及处理器可用的缓存量,但是这是一个理论上的担忧。


切换齿轮,You can see。有趣!两个测试函数的代码看起来非常不同。使用printf,Clang完全展开了内循环,并向Test2函数发出了10次背靠背调用。然后将其包裹在旋转100次的循环内。同样,它与您最初编写的C代码一致,但由于内部循环的迭代次数很少,因此Clang的优化器确定展开它可能是性能上的胜利。它可能是对的。 Test2发生了什么?嗯,有点相同 - 它只是以不同的方式展开它,因为你以不同的方式编写了原始代码。它展开了外部循环,提供10个从0到100循环的背对背代码序列。

继续我们打破性能分析基本规则的主题,我们将跳过对输出的基准测试,并从概念上思考它。跳出来的第一件事是Test1需要 lot 更多的代码 - 它需要两倍多的字节来编码这些指令(321对141字节)。当然,较小的代码并不总是更快,但是在这里,除非是明显的赢家,否则我倾向于向更小的代码方向犯错。唯一可能影响该分析的是,Test2中展开循环体内的代码量是否太大而无法放入缓存中。 {{1}}中的循环体 更小,即使整体代码更大,因此它们几乎保证在缓存中很热。将代码放在缓存中方式可以提高性能。嗯,我想我们毕竟无法在没有基准测试的情况下告诉我们。


总结:

  • 通过基准测试回答性能问题。
  • 始终基准实际代码,而不是任意测试用例(因为生成提供有意义结果的正确案例非常困难)。
  • 理论上,完美的优化编译器应该将这些片段转换为相同的代码 在实践中,这可能不是真的。根据您编写代码的方式,不同的编译器会发出略有不同的代码。但是,所有这些都会生成非常敏感的代码,遵循您在原始C源中设置的引导。
  • 同样,从理论上讲,这些应该具有相同的性能。实际上,它可能稍微复杂一些。但在实践中,它并不重要,因为两者都足够快。我们所说的差异大约为纳秒级。你在浪费时间担心这件事。