【问题标题】:C compiler optimize loop by running itC编译器通过运行它来优化循环
【发布时间】:2015-07-14 23:06:24
【问题描述】:

C 编译器能否通过运行循环来优化循环?

例如:

int num[] = {1, 2, 3, 4, 5}, i;
for(i = 0; i < sizeof(num)/sizeof(num[0]); i++) {
  if(num[i] > 6) {
    printf("Error in data\n");
    exit(1);
  }
}

不是每次执行程序时都运行这个,编译器可以简单地运行它并优化它吗?

【问题讨论】:

  • 嗯,你可以和 ex 核对一下。 gcc -S。这称为“循环展开”;编译器有效地复制+粘贴循环体,每次将i 变量更改为常量。 AFAIK 它适用于小循环,并且可以允许发生其他优化(例如消除 if 分支会生成的死代码)。
  • 尝试使用-S -funroll-loops 编译并检查输出。不过,“运行”循环并不是一个完全正确的术语——它不像实际执行的代码。
  • 查找“基于配置文件的优化”或 PBO(又名“配置文件引导优化”或 PGO)。是的,优化器可以根据观察到的程序运行性能进行优化。另请参阅“链接时优化”(LTO)和“整个程序优化”。

标签: c loops for-loop optimization compiler-optimization


【解决方案1】:

让我们看看……(这确实是唯一的判断方法。)

拳头,我已将您的 sn-p 转换为我们可以实际尝试编译和运行的东西,并将其保存在一个名为 main.c 的文件中。

#include <stdio.h>

static int
f()
{
  const int num[] = {1, 2, 3, 4, 5};
  int i;
  for (i = 0; i < sizeof(num) / sizeof(num[0]); i++)
    {
      if (num[i] > 6)
        {
          printf("Error in data\n");
          return 1;
        }
    }
  return 0;
}

int
main()
{
  return f();
}

运行gcc -S -O3 main.c 会生成以下程序集文件(在main.s 中)。

        .file   "main.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB0:
        .section        .text.startup,"ax",@progbits
.LHOTB0:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB22:
        .cfi_startproc
        xorl    %eax, %eax
        ret
        .cfi_endproc
.LFE22:
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE0:
        .section        .text.startup
.LHOTE0:
        .ident  "GCC: (GNU) 5.1.0"
        .section        .note.GNU-stack,"",@progbits

即使您不了解汇编,您也会注意到字符串 "Error in data\n" 不存在于文件中,因此显然必须进行某种优化。

如果我们仔细观察为main 函数生成的机器指令,

xorl    %eax, %eax
ret

我们可以看到,它所做的只是将 EAX 寄存器与自身进行异或运算(结果始终为零)并将该值写入 EAX。然后它再次返回。 EAX 寄存器用于保存返回值。正如我们所见,f 函数被完全优化掉了。

【讨论】:

  • 有趣的是,clang(在 3.5.1 上测试)只需 -O1 即可实现这一目标。
  • 假设您有一个确定性程序需要很长时间才能运行(也许num 很长,但已编译)。编译器在什么时候说“我现在要优化这个”,而不是“让它运行”?
  • @Ariel 除了查看为您的程序实际生成的汇编代码之外,没有可靠的方法可以判断,如果您稍作更改,可能会发生不同的事情。至少除非您知道编译器的详细信息。看看this question 和我的回答,举个例子,推断编译器最终决定执行什么优化是多么棘手。
【解决方案2】:

【讨论】:

  • 我不知道你为什么需要在这里展开任何循环。只需要注意代码什么都不做。
  • 不是展开编译器执行的过程的循环部分以查看代码什么都不做吗?
  • 我不明白为什么。你可以很容易地意识到循环体是空的(因为条件总是假的)而不展开......
  • 是的,我可以实现,但我不是编译器。编译器是如何实现的?
  • 如果你能实现它,当然可以对机器进行编程来实现同样的事情:-)
【解决方案3】:

您没有指定编译器,但使用带有 -O3 的 gcc 在 for 可能之外进行大小计算,它可以做一些调整。

【讨论】:

    【解决方案4】:

    编译器可以做得更好。编译器不仅可以检查“正向”运行代码的效果,而且标准甚至允许他们在涉及潜在未定义行为的情况下反向工作代码逻辑。例如,给定:

    #include <stdio.h>
    int main(void)
    {
      int ch = getchar();
      int q;
      if (ch == 'Z')
        q=5;
      printf("You typed %c and the magic value is %d", ch, q);
      return 0;
    }
    

    编译器有权假设程序永远不会收到任何输入,这会导致在q 没有收到值的情况下到达printf;由于唯一会导致q 接收值的输入字符是'Z',因此编译器可以合法地将代码替换为:

    int main(void)
    {
      getchar();
      printf("You typed Z and the magic value is 5");
    }
    

    如果用户键入Z,则原程序的行为将被明确定义,而后者的行为将与之匹配。如果用户键入任何其他内容,原始程序将调用未定义行为,因此,标准不会对编译器可能执行的操作提出任何要求。编译器将有权做任何它喜欢的事情,包括产生与键入 Z 产生的结果相同的结果。

    【讨论】:

    • 但这并不能真正回答 OP 的问题。
    • 什么“第二个printf”?每个代码sn-p中只有一个。
    • @ThePcLuddite:是不是好一点?
    • @void_ptr:过失。我简化了我的示例并忽略了修复要匹配的文本。
    猜你喜欢
    • 2012-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-17
    • 2020-06-14
    • 1970-01-01
    • 2013-02-26
    • 1970-01-01
    相关资源
    最近更新 更多