为什么当限制是959而不是960时,一个简单的循环是优化的?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (48)

考虑一下这个简单的循环:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

如果使用gcc 7(快照)或clang(主干)编译-march=core-avx2 -Ofast

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

换句话说,它只是将答案设置为960而不循环。

但是,如果将代码更改为:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

生成的程序集实际上执行循环和?例如,clang给出:

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

为什么这和为什么对clang和gcc是完全一样的呢?

如果换float带着double是479。对于gcc和clang来说也是如此。

更新1

结果表明,gcc 7(快照)和clang(主干)的行为非常不同。在我所能指出的范围内,clang为所有小于960的限制优化了循环。gcc对确切值很敏感,没有上限。

提问于
用户回答回答于

GCC版本<=6.3.0

gcc的相关优化选项是-fpeel-loops,它与标志一起间接启用。-Ofast

有足够的信息而不滚动的剥离循环(来自配置文件反馈或静态分析)。它还开启全环剥离(如:迭代次数较小的循环的完全去除)。 启用-O3和/或-fprofile-use...

更多细节可以通过添加-fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely
if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

因此函数try_peel_loop返回false.

更详细的输出可以用-fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

可以通过与max-completely-peeled-insns=nmax-completely-peel-times=nParams:

最大---全脱皮--- 一个完全剥离的循环的最大进给量。 最大-完全-剥离次数 循环的最大迭代次数,适合于完全剥离。

例如,如果使用以下选项进行编译:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

然后代码变成:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

我不知道Clang实际做了什么以及如何调整它的限制,克完全移除它:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

成果分为:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret
用户回答回答于

  1. 如果循环计数器为常数(且不太高),编译器将完全展开循环。
  2. 一旦展开,编译器就会看到SUM操作可以分组为一个。

如果由于某种原因没有展开循环(这里:它将生成太多的语句)1000),操作不能分组。

编译器确保1000条语句的展开相当于一个加法,但是上面描述的步骤1和2是两个单独的优化,所以它不能冒展开的“风险”,不知道是否可以分组操作(例如:函数调用不能分组)。

扫码关注云+社区