【问题标题】:Unrolling small loops with Visual Studio 2005使用 Visual Studio 2005 展开小循环
【发布时间】:2009-09-02 15:33:50
【问题描述】:

如何根据迭代次数或其他属性告诉编译器unroll loops?或者,如何在 Visual Studio 2005 中启用循环展开优化

编辑:例如

//Code Snippet 1
    vector<int> b;
    for(int i=0;i<3;++i) b.push_back(i);

相对

//Code Snippet 2
    vector<int> b;
    b.push_back(0);
    b.push_back(1);
    b.push_back(2);

push_back() 是一个例子,我可以用任何可能需要很长时间的东西来替换它。

但我在某处读到我可以使用代码 1,如果循环满足某些条件,编译器可以将其展开为代码 2。所以我的问题是:你是怎么做到的?已经有关于 SO 的讨论,关于哪个更有效,但无论如何,任何 cmets 都会受到赞赏。

【问题讨论】:

  • “展开”是什么意思?
  • 我想你有一个具体的循环,你通过测量发现循环需要很长时间。你为什么不把那个循环贴出来让我们看看?
  • push_back 的开销可能比循环的开销大一个数量级。这是试图优化错误事物的一个很好的例子。
  • 澄清所有这些好的答案:别担心。它不会有什么不同,而且你不可能知道,直到你在真实情况下测试它。您假设您比编译器了解更多,这是一件坏事。
  • 请:这个问题很重要!这个例子应该解释他想说什么,而不是一个例子,它是有意义的。你们都说展开循环根本没有意义吗?如果没有,那么请帮助我们需要知道它是如何完成的。这个问题,你应该什么时候做,这里没有被问到。

标签: c++ visual-studio-2005 compiler-construction visual-c++-2005


【解决方案1】:

通常你只是让编译器完成它的工作。如果在编译时知道循环的数量,并且打开了编译器优化,编译器将平衡代码大小和分支缩减,并展开所有不可滚动的循环。

如果这真的不是你想要的,也可以用 Duff 的设备自己做:(来自维基百科)

send(to, from, count)
register short *to, *from;
register count;
{
    register n=(count+7)/8;
    switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
        }while(--n>0);
    }
}

这使您可以使用运行时确定的迭代计数展开。

如果它仍然是你想要的编译时展开,并且内置优化不是你想要的(如果你想要更细粒度的控制),你可以创建一个 C++ 模板来做你想做的事。这是一个非常简单的模板应用程序,因为它都是在编译时完成的,所以您不会丢失任何函数内联或编译器可能额外执行的其他优化。

【讨论】:

  • 请注意,Tom Duff 自己称这件事“令人作呕”并说:“我对这一发现感到既自豪又厌恶”(groups.google.com/group/net.lang.c/msg/66008138e07aa94c)我想当前编译器的优化器很少这样做的好处(如果有的话),所以除非你有一个证明有帮助的案例,否则请远离。 (还要注意,上面是 K&R C,不应该用 C++ 编译器编译,register 可能被所有当前的 C++ 编译器忽略。)
  • 这些天你真的很难超越编译器 - 到目前为止,在 5 年的后端服务器工作中,我从未见过需要 Duff 的设备。我们有一个需要模板解决方案的案例——通过调整目标服务器的缓存大小,我们设法在一个特别瓶颈的操作中获得了 10 倍的加速。
  • 为什么这被否决了?对我来说似乎很好。它说“不要打扰”,并解释了在您希望手动确保展开的极少数情况下该怎么做。
  • 我认为将其标记下来很苛刻,答案说您通常应该让编译器完成它的工作。
  • 我的 OP 是关于 VS2005 中的那些编译器优化。
【解决方案2】:

这通常相当简单:“您启用优化”。

如果您告诉编译器优化您的代码,那么循环展开是它尝试应用的众多优化之一。

但请记住,展开并非总是会产生更快的代码。它可能会导致缓存未命中(在数据和指令缓存中)。并且在现代 CPU 中发现了高级分支预测,构成循环的分支的成本通常可以忽略不计。

有时,编译器可能会确定展开会产生较慢的代码,然后它不会这样做。

【讨论】:

  • 对,但是怎么做呢?一个 VS2005 的具体答案将是有益的
  • 除非您在 Microsoft 的编译器优化团队工作,否则无法具体回答该问题。文档非常模糊。除非您只想知道如何开启优化? (RTFM)
  • 如果开启优化可以实现这一点,那么这不是问题。但是我在某处读到,用户可以指定在展开循环之前需要多少次迭代,并且想知道这里是否有人知道这一点。但是如果编译器像 jalf 所说的那样估计时间等,那么我想打开优化就足够了。
  • Visual Studio 让您可以控制内联深度 (#pragma inline_depth),但不能控制循环展开。
  • 编译器会尝试估计它。但请记住,它必须是编译时的决定。它不能让循环运行 X 次迭代,然后 然后 决定展开。因此,如果迭代次数是编译时常量,编译器将能够更好地确定何时以及如何展开。如果迭代次数是在运行时确定的,那么编译器的选项就会受到更多限制——然后您可能必须手动展开循环。 (当然,在分析显示性能是一个问题之后)
【解决方案3】:

循环展开不会神奇地使循环中执行的代码运行得更快。它所做的只是节省一些用于比较循环变量的 CPU 周期。因此,只有在循环体本身几乎什么都不做的非常紧凑的循环中才有意义。

关于您的示例:虽然push_back() 需要摊销的常数时间,但这确实包括偶尔的分配-复制-解除分配周期以及实际对象的复制。我非常怀疑循环中的比较与此相比起重要作用。如果你用其他需要很长时间的东西替换它,同样适用。

当然,这在任何特定 CPU 上都可能是错误的,而在任何其他 CPU 上都是正确的。由于现代 CPU 架构及其缓存、指令流水线和分支预测方案的特性,在优化代码方面很难超越编译器。您会尝试通过展开来优化具有“沉重”主体的循环,这似乎暗示您对此知之甚少。 (我很努力的这么说,以免你被冒犯。我是第一个承认我自己在这个游戏中比较松的人。)

如果您遇到性能问题,IME 在 10 个案例中有 9 个消除愚蠢的错误(如复制复杂对象)并优化算法和数据结构是您应该关注的。

(如果您仍然认为您的问题属于 1-out-of-10 类别,那么请尝试 Intel 的编译器。上次我看它时,您可以免费下载测试版本,它插入 VS,非常易于设置,并在我测试过的应用程序中带来了 0.5% 的速度增益。)

【讨论】:

  • “它所做的只是节省一些用于比较循环变量的 CPU 周期”——这根本不是真的。展开可以允许对具有长依赖链的指令流进行超标量执行(当然,如果迭代是独立的)。
【解决方案4】:

注意你说:

push_back() 是一个例子,我可以用任何可能需要很长时间的东西来替换它。

事实上,如果 push_back()(或任何你替换它的东西)需要很长时间,那么循环展开就会浪费精力。循环通常不是特别慢。循环展开有意义的时候是循环内完成的工作非常小 - 在这种情况下,循环结构可能开始主导该段执行的处理。

我相信你会得到许多其他答案 - 除非你真的发现它是一个瓶颈,否则不要担心这种类型的事情。 99% 的时候不会。

【讨论】:

    【解决方案5】:

    右键单击项目,选择属性并导航: alt text http://img200.imageshack.us/img200/8685/propsm.jpg

    WRT 循环展开,请注意,由于缓存命中/未命中,MS Visual Studio 优化大小而不是速度实际上会产生更快的代码,这一点已被普遍接受。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-12
      • 1970-01-01
      • 2010-09-19
      • 2012-07-04
      相关资源
      最近更新 更多