C++循环展开用于编译时常量小值

3

我有这两个函数:

template<int N>
void fun()
{
    for(int i = 0; i < N; ++i)
    {
        std::cout<<i<<" ";
    }
}

void gun(int N)
{
    for(int i = 0; i < N; ++i)
    {
        std::cout<<i<<" ";
    }
}

我可以假设在第一个版本中,编译器将为每个小N(我指的是N = {1, 2, 3, 4})优化循环吗?


2
你期望哪种优化,大小还是速度? - masoud
@MM。速度。在第一种情况下大小将会更大,因为会生成更多的函数并且如果编译器展开循环,则函数大小可能会更大。 - Mircea Ispas
@MatsPetersson 是唯一一个明白循环内部内容的人。cout 的执行时间比展开循环所能节省的时间要长得多。 - Mike Dunlavey
5个回答

3

我可以假设在第一个版本中,编译器会为每个小的N进行优化。

这是一种典型的优化方式,虽然“假设”是一个强烈的词。如果某个优化是必要的,任何可能的优化最终都会让你失望。

如果编译器能够内联函数,您的第二个版本也可能经历相同的优化。


为什么编译器不会对两者进行优化呢?好像也没有别名指针指向 i - Antimony
4
由于if函数在另一个翻译单元中,编译器将无法将其内联,而且N在编译时是未知的。 - kassak

2

通常情况下,使用适当的优化级别,虽然无法保证优化会做出什么样的改变,但你可以相信它会比手动优化更好。

如果你真的想了解生成的代码是什么,你可以查看生成的汇编代码。


1
这取决于您的优化级别和标志。使用-O0 -g(无优化,启用调试)与-O3(大力优化以提高速度)以及-Os(优化空间)之间存在很大差异。
现在,即使是在优化速度时,循环展开也不一定是一个胜利者。过多的代码可能会导致指令缓存未命中,这将远远超过内联简单循环的加速效果。而像这样的循环中条件分支的成本几乎可以忽略不计,因为分支预测将正确地预测除最后一次迭代外的所有情况。

1
如果编译器可以内联其中任一函数,它也会展开循环,如果它认为这是正确的事情的话。编译器何时以及如何决定展开循环具有相当复杂性,并且高度依赖于其他因素,例如可用寄存器的数量,循环内发生的事情(例如,上面给出的示例可能不会从减少循环中涉及的5个或更多指令中获得太多时间优势,因为cout ...可能会消耗数千倍的时间 - 编译器是否能够弄清楚这一点还是另一回事,但并非完全没有编译器对函数是否小型有一定的理解。

另一方面,如果代码看起来像这样:

int arr[N];  // Global array. 

template<int N>
int fun()
{
    int sum = 0;
    for(int i = 0; i < N; ++i)
    {
        sum += arr[i];
    }
}

那么我希望编译器可以将循环展开,类似于以下代码:

    int *tmp = arr;
    sum += *tmp++;
    sum += *tmp++;
    sum += *tmp++;
    sum += *tmp++;
    sum += *tmp++;

假设N = 5。对于任何在编译器中“可见”的函数,只要N在编译时是已知的,都适用此规则。因此,假设gun不在不同的源文件中,则我期望它与fun(作为模板函数,必须在此编译单元中可见)被内联和展开的方式完全相同。

0

如果你想更加明确一些,可以使用Duff's Device,它使用switch case fallthrough来展开循环。但我无法保证它在实践中的效果如何。不过我想,如果你能提示编译器来展开循环,那应该会更快。

编译器也非常聪明,虽然它们并非万无一失,但它们的优化选择通常比我们自己的直觉更好。


现在使用Duff's device仍然是一个好主意吗?它是几十年前发明的,当时编译器还非常基础,没有进行很多优化。目前,如果它们实际上会影响编译器的正确优化能力,我不会感到惊讶。那么,您是否知道有人已经在现代编译器上对此进行了调查? - Agentlien
我也在想同样的问题。我想提出它作为一个需要研究的东西,但是我想编译器展开(compiler unrolling)会更快。特别是如果它可以在运行时决定N,那么就会有更少的比较。 - Dan Lecocq

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接