【问题标题】:Why -O1 is faster than -O2 for 10000 times?为什么 -O1 比 -O2 快 10000 倍?
【发布时间】:2015-03-02 10:09:26
【问题描述】:

下面是一个计算多项式的 C 函数:

/* Calculate a0 + a1*x + a2*x^2 + ... + an*x^n */
/* from CSAPP Ex.5.5, modified to integer version */
int poly(int a[], int x, int degree) {
  long int i;
  int result = a[0];
  int xpwr = x;
  for (i = 1; i <= degree; ++i) {
    result += a[i]*xpwr;
    xpwr *= x;
  }
  return result;
}

还有一个主要功能:

#define TIMES 100000ll
int main(void) {
  long long int i;
  unsigned long long int result = 0;
  for (i = 0; i < TIMES; ++i) {
    /* g_a is an int[10000] global variable with all elements equals to 1 */
    /* x = 2, i.e. evaluate 1 + 2 + 2^2 + ... + 2^9999 */
    result += poly(g_a, 2, 9999);
  }
  printf("%lld\n", result);
  return 0;
}

当我用GCC和选项-O1和-O2分别编译程序时,我发现-O1比-O2快很多。

平台详情:

  • i5-4600
  • 带有内核 3.18 的 Arch Linux x86_64
  • GCC 4.9.2
  • gcc -O1 -o /tmp/a.out test.c
  • gcc -O2 -o /tmp/a.out test.c

结果:

  • 当 TIMES = 100000ll 时,-O1 立即打印结果,而 -O2 需要 0.36s
  • 当 TIMES = 1000000000ll 时,-O1 在 0.28 秒内打印结果,-O2 耗时太长,以至于我没有完成测试

似乎 -O1 比 -O2 快大约 10000 倍。

当我在 Mac (clang-600.0.56) 上测试它时,结果更加奇怪:-O1 即使在 TIMES = 1000000000000000000ll 时也不超过 0.02s

我已经测试了以下更改:

  • 使 g_a 随机(元素从 1 到 10)
  • x = 19234(或其他数字)
  • 使用 int 而不是 long long int

结果是一样的。

我试图查看汇编代码,似乎 -O1 正在调用 poly 函数,而 -O2 正在执行内联优化。但是内联应该会让性能更好,不是吗?

是什么造成了这些巨大的差异?为什么clang上的-O1可以让程序这么快? -O1 做错了吗? (我无法检查结果,因为没有优化它太慢了)

【问题讨论】:

    标签: c compiler-optimization


    【解决方案1】:

    这是-O1main的汇编代码:(你可以通过在gcc中添加-S选项来得到它)

    main:
    .LFB12:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $9999, %edx
        movl    $2, %esi
        movl    $g_a, %edi
        call    poly
        movslq  %eax, %rdx
        movl    $100000, %eax
    .L6:
        subq    $1, %rax
        jne .L6
        imulq   $100000, %rdx, %rsi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
    

    对于-O2

    main:
    .LFB12:
        .cfi_startproc
        movl    g_a(%rip), %r9d
        movl    $100000, %r8d
        xorl    %esi, %esi
        .p2align 4,,10
        .p2align 3
    .L8:
        movl    $g_a+4, %eax
        movl    %r9d, %ecx
        movl    $2, %edx
        .p2align 4,,10
        .p2align 3
    .L7:
        movl    (%rax), %edi
        addq    $4, %rax
        imull   %edx, %edi
        addl    %edx, %edx
        addl    %edi, %ecx
        cmpq    $g_a+40000, %rax
        jne .L7
        movslq  %ecx, %rcx
        addq    %rcx, %rsi
        subq    $1, %r8
        jne .L8
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC1, %edi
        xorl    %eax, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
    

    虽然我对汇编不太了解,但很明显-O1 只是调用了一次poly,然后将结果乘以100000(imulq $100000, %rdx, %rsi)。这就是它如此之快的原因。

    gcc 似乎可以检测到poly 是一个没有副作用的纯函数。 (如果我们在 poly 运行时有另一个线程修改 g_a 会很有趣......)

    另一方面,-O2 内联了poly 函数,因此它没有机会将poly 作为纯函数进行检查。

    我进一步做了一些研究:

    我找不到 -O1 使用的实际标志,它执行纯函数检查。

    我已经单独尝试了gcc -Q -O1 --help=optimizers 列出的所有标志,但没有一个有效果。

    也许需要将flags组合在一起才能达到效果,但是很难尝试所有的组合。

    但是我找到了-O2使用的使效果消失的标志,即-finline-small-functions标志。旗帜的名称本身就说明了问题。

    【讨论】:

    • 现代 gcc 仍然具有相同的行为,即使使用 __attribute__((pure))。但是__attribute__((pure,noinline)) 将让gcc -O3 调用一次函数并且根本不运行外循环。 godbolt.org/g/EAD5a4。不错的侦探工作,有趣的错过优化。 IDK 为什么 gcc 在内联后不将内循环从外循环中提升出来,除了可能是因为人们通常只对微基准重复循环这样做。
    • 请注意,result = (result * x) + a[i]; 在从最后一个系数倒数的循环中也可以工作,但在关键路径(循环携带的依赖链)中有一个乘法和一个加法。在具有流水线乘法硬件的现代 CPU 上,每次迭代执行两次乘法实际上可能会更好。
    【解决方案2】:

    让我大吃一惊的是你溢出了有符号整数。这种行为在 C 中是未定义的。具体来说,int result 将无法保存 pow(2,9999)。我看不出用未定义行为对代码进行基准测试有什么意义?

    【讨论】:

    • x = 1 怎么样?我尝试使用 scanf 获取 x,然后将其传递给 poly 调用,结果是相同的。我认为这不是主要问题...
    猜你喜欢
    • 2021-03-02
    • 2019-11-29
    • 1970-01-01
    • 2016-07-07
    • 1970-01-01
    • 2013-06-06
    • 2018-08-12
    • 1970-01-01
    • 2011-08-14
    相关资源
    最近更新 更多