繁体   English   中英

c中的嵌套循环效率

[英]Nested loop efficiency in c

哪个更好,为什么?

情况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");

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

编辑

我用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;

假设我们谈论的是在启用优化的情况下编译代码的情况,因为禁用优化时谈论“效率”或“性能”毫无意义……

这些编译为相同的目标代码。 所有循环边界都是编译时常量,因此,编译器理论上可以确定循环主体中的代码将被执行多少次,将所有内容折叠到单个循环中,然后发出该代码。 如果它愿意(并且不希望这样做,因为这是非常愚蠢的,并且不能显着提高速度),它可以发出10,000个对printf函数的顺序调用。 这只是基本的循环展开,如今几乎所有优化的编译器都可以做到这一点。

在现实世界中,编译器并没有表现出魔力(并且通常不会对其进行优化以优化哑代码或识别其模式),因此这些代码片段实际上将编译为稍有不同的目标代码。


查看GCC的输出,它将标准窥视孔优化应用于循环,但不合并它们。 它还按照您编写循环的方式进行循环。 您可以看到Test1Test2的代码基本相同,除了Test1在外部循环中运行大约100倍,在内部循环中运行大约10倍,而Test2则相反。 这只是将不同的常量移到寄存器中的问题。

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

性能如何? 好吧,回答这个问题的唯一正确方法是同时编译样本和检查。 实际上,这是您获得性能问题的客观答案的唯一途径。 但是,如果尝试这样做,您将立即遇到一个问题:循环中的printf函数将极大地控制其他任何事情所花费的时间,从而导致基准测试结果嘈杂且毫无意义。 您需要找出在循环内执行的其他操作,这些操作不会对尝试测量的时间产生太大的影响,而且还必须具有一些副作用,这些副作用会阻止编译器执行简单地对其进行优化。 这就是为什么这样的微基准很难正确完成的原因。 它们也不是特别有趣。 您应该进行基准测试的是真实代码 这不是真实的代码。 因此,我什至都不会试图从中获取有意义的基准数字。

我唯一要允许自己做的就是稍微了解一下为这两个函数生成的代码在概念上的性能含义。 猜想将较大的循环设为内部循环( Test2 )会稍微快一些。 为什么? 好吧,因为一旦代码被加载到指令高速缓存中,它就会快速执行100次,而分支预测器几乎可以在所有情况下成功预测分支的目标。 这与紧密循环一样有效。 在另一种情况下,您只能在这些最佳条件下进行10次迭代,然后再忍受并重新开始,这有可能会从指令高速缓存中逐出代码。 您必须测试和/或研究代码的细节,以查看是否确实存在这种可能性,因为这取决于代码的确切大小以及处理器可用的缓存量,但这是理论上的问题。


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

继续以打破性能分析的基本规则为主题,我们将跳过对输出进行基准测试,仅在概念上进行思考。 即跳出的第一件事是, Test2需要更多代码,它需要两倍以上的多字节编码的指令(321与141个字节)。 当然,较小的代码并不一定总是更快,但是在这里,如果没有其他明显的赢家,我倾向于朝着较小的代码的方向发展。 唯一可能影响该分析的是, Test1中展开的循环体内的代码量是否太多而无法容纳在缓存中。 在循环体Test2 小得多,即使整体代码比较大,所以他们几乎可以肯定是在缓存中的热点。 将代码存储在缓存中可以更好地提高性能。 嗯,我想毕竟如果没有基准测试,我们将无法分辨。


综上所述:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM