9

如何说服 GCC 展开一个迭代次数已知但很大的循环?

我正在用-O3.

当然,有问题的实际代码更复杂,但这里有一个具有相同行为的简化示例:

int const constants[] = { 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144 };

int get_sum_1()
{
    int total = 0;
    for (int i = 0; i < CONSTANT_COUNT; ++i)
    {
        total += constants[i];
    }
    return total;
}

...如果CONSTANT_COUNT定义为 8(或更少),则 GCC 将展开循环,传播常量,并将整个函数简化为简单的return <value>;. 另一方面,如果CONSTANT_COUNT是 9(或更大),则循环不会展开,并且 GCC 会生成一个二进制文件,该二进制文件会循环、读取常量并在运行时添加它们——尽管理论上该函数仍然可以优化到只返回一个常数。(是的,我查看了反编译的二进制文件。)

如果我手动展开循环,如下所示:

int get_sum_2()
{
    int total = 0;
    total += constants[0];
    total += constants[1];
    total += constants[2];
    total += constants[3];
    total += constants[4];
    total += constants[5];
    total += constants[6];
    total += constants[7];
    total += constants[8];
    //total += constants[9];
    return total;
}

或这个:

#define ADD_CONSTANT(z, v, c) total += constants[v];

int get_sum_2()
{
    int total = 0;
    BOOST_PP_REPEAT(CONSTANT_COUNT, ADD_CONSTANT, _)
    return total;
}

...然后函数被优化为返回一个常数。因此,一旦展开,GCC 似乎能够处理较大循环的持续传播;挂断似乎只是让 GCC 考虑首先展开更长的循环。

但是,手动展开也不BOOST_PP_REPEAT是可行的选项,因为在某些情况下CONSTANT_COUNT是运行时表达式,并且相同的代码仍然需要在这些情况下正常工作。(在这些情况下,性能并不那么重要。)

我正在使用 C(不是 C++),所以模板元编程也不constexpr可用。

我已经尝试过-funroll-loops, -funroll-all-loops, , 并为, , , , , ,-fpeel-loops和 设置较大的值,但似乎没有任何区别。max-unrolled-insnsmax-average-unrolled-insnsmax-unroll-timesmax-peeled-insnsmax-peel-timesmax-completely-peeled-insnsmax-completely-peel-times

我在 Linux x86_64 上使用 GCC 4.8.2。

有任何想法吗?是否有我缺少的标志或参数......?

4

2 回答 2

3

我不确定此解决方法是否适用于您的实际问题,但我发现运行 Parabola GNU/Linux 的 x86_64 上的 GCC 4.9.0 20140604(预发行版)将以下循环展开并包括CONSTANT_COUNT == 33.

int
get_sum()
{
  int total = 0;
  int i, j, k = 0;
  for (j = 0; j < 2; ++j)
    {
      for (i = 0; i < CONSTANT_COUNT / 2; ++i)
        {
          total += constants[k++];
        }
    }
  if (CONSTANT_COUNT % 2)
    total += constants[k];
  return total;
}

我只是通过它的-O3标志。的汇编代码get_sum真的只是

movl $561, %eax
ret

我没有尝试,但也许模式可以进一步扩展。

这对我来说似乎很奇怪,因为——至少在我看来——代码现在看起来要复杂得多。不幸的是,这是强制展开的一种相当侵入性的方式。编译器标志会更好。

于 2014-09-17T02:20:48.910 回答
1

GCC 有很多关于循环展开(和优化)的模糊参数和程序参数。您可以使用-funroll-loops, -funroll-all-loops, --param name=value , (例如,名称max-unroll-times....)等。

争论的顺序gcc很重要。你可能想把它-O3放在第一位,然后是上面的奇怪选项。

但是,增加展开并不总是会提高性能。

最后但同样重要的是,您可以编写自己的 GCC 插件来更改展开标准。

巧妙使用__builtin_prefetch可能会提高性能,请参阅this answer(但不小心使用会降低性能)

您需要进行基准测试。我的感觉是,过早的微优化会浪费你的时间。

于 2014-09-18T19:37:45.657 回答