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


131

考虑以下简单循环:

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和铛一样了。

更新1

事实证明,gcc 7(快照)和clang(树干)的行为非常不同。据我所知,clang会针对所有小于960的限制优化循环。另一方面,gcc对精确的值敏感,没有上限。例如,它优化圈外时的极限是200(以及许多其他的值),但它确实当极限是202和20002(以及许多其他的值)。


3
Sulthan可能的意思是:1)编译器展开循环,2)展开后,可以将求和运算分组为一个。如果未展开循环,则无法将操作分组。
让·弗朗索瓦·法布尔

3
具有奇数个循环会使展开变得更加复杂,必须特别完成最后几次迭代。这可能足以使优化器进入无法再识别快捷方式的模式。很可能,它首先必须为特殊情况添加代码,然后必须再次将其删除。在耳朵之间始终使用优化器总是最好的:)
Hans Passant

3
@HansPassant它还针对小于959的任何数字进行了优化
。– eleanora

6
通常不是通过归纳变量消除来完成此操作,而不是展开疯狂的操作吗?展开959倍简直太疯狂了。
哈罗德

4
@eleanora我曾与该编译资源管理器一起玩,但以下内容似乎成立(仅谈论gcc快照):如果循环计数是4的倍数且至少为72,则该循环不会展开(或更确切地说,被循环展开)因子4); 否则,整个循环将被一个常量替换-即使循环计数为2000000001。我的怀疑:过早的优化(例如,过早的“嘿,是4的倍数,这对于展开非常有用”)阻止了进一步的优化与a更彻底的“反正这个循环有什么关系?”)
哈根·冯·埃森

Answers:


88

TL; DR

默认情况下,当前快照GCC 7的行为会不一致,而由于PARAM_MAX_COMPLETELY_PEEL_TIMES,以前的版本具有默认限制,即16。可以从命令行覆盖它。

限制的基本原理是防止过于激进的循环展开,这可能是一把双刃剑

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

消息来自/gcc/tree-ssa-loop-ivcanon.c

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=nand max-completely-peel-times=n参数修饰来限制:

max-completely-peeled-insns

完全剥离的循环的最大insns数。

max-completely-peel-times

循环的最大迭代次数适合完全剥离。

要了解有关insns的更多信息,请参阅《GCC内部手册》

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

-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实际做什么以及如何调整其限制,但是正如我观察到的那样,您可以通过将循环标记为unroll pragma来迫使它评估最终值,它将完全删除它:

#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

谢谢你这个很好的答案。正如其他人指出的那样,gcc似乎对确切的限制大小很敏感。例如,它无法消除912 godbolt.org/g/EQJHvT的循环。在这种情况下,fdump-tree-cunroll-details怎么说?
eleanora

实际上,甚至200个也有此问题。这一切都在Godbolt提供的gcc 7快照中。godbolt.org/g/Vg3SVs 这根本不适用于clang。
eleanora

13
您解释了剥离的机理,但没有解释960的相关性,或者根本没有限制的原因
MM

1
@MM:GCC 6.3.0和最新的snaphost之间的剥离行为完全不同。在前者的情况下,我强烈怀疑,认为硬编码限制被强制PARAM_MAX_COMPLETELY_PEEL_TIMESPARAM,即定义/gcc/params.def:321与价值16
格热戈日Szpetkowski

14
您可能要提到为什么 GCC故意以此方式限制自己。具体来说,如果您过分地展开循环,则二进制文件会变大,并且不太可能适合L1缓存。假设良好的分支预测(对于典型的循环而言),则缓存未命中相对于保存一些条件跳转而言可能会非常昂贵
凯文(Kevin)

19

阅读Sulthan的评论后,我猜是:

  1. 如果循环计数器为常数(且不是太高),则编译器将完全展开循环

  2. 展开后,编译器发现可以将求和运算分组为一个。

如果循环由于某种原因未展开(此处:会使用生成太多语句1000),则无法对操作进行分组。

编译器可以看到,展开1000条语句等于一次加法,但是上述步骤1和步骤2是两个单独的优化,因此,它无法承担展开的“风险”,不知道是否可以对操作进行分组(示例:函数调用无法分组)。

注意:这是一个极端的情况:谁使用循环再次添加相同的内容?在这种情况下,不要依赖编译器可能的展开/优化;直接在一条指令中编写正确的操作。


1
那你可以专注于那not too high一部分吗?我的意思是为什么在这种情况下不存在风险100?我在上面的评论中猜到了什么……这可能是原因吗?
user2736738

我认为编译器并不了解它可能触发的浮点错误。我想这只是指令大小的限制。你有max-unrolled-insns一起max-unrolled-times
让·弗朗索瓦·法布尔

嗯,这是我的想法或猜测...希望获得更明确的理由。
user2736738

5
有趣的是,如果将其更改floatint,则gcc编译器由于其归纳变量优化(-fivopts)而能够降低循环的强度,而无需考虑迭代次数。但是那些似乎对floats 无效。
塔维安·巴恩斯

1
@CortAmmon对,我回想起有些人感到惊讶和不安的是,GCC使用MPFR来精确地计算非常大的数字,其结果与等效的浮点运算完全不同,因为浮点运算会累积误差和精度损失。旨在表明许多人错误地计算了浮点数。
Zan Lynx '02

12

很好的问题!

您似乎在简化代码时,编译器尝试内联的迭代次数或操作数量已达到极限。正如Grzegorz Szpetkowski所记录的那样,有一些编译器特定的方法可以通过编译指示或命令行选项来调整这些限制。

您还可以使用Godbolt的Compiler Explorer来比较不同的编译器和选项如何影响所生成的代码:gcc 6.2并且icc 17仍内联960,而clang 3.9不是(对于默认的Godbolt配置,它实际上在73处停止内联)。


我已经对问题进行了编辑,以使其清楚我正在使用的gcc和clang的版本。参见godbolt.org/g/FfwWjL。例如,我正在使用-Ofast。
eleanora
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.