【问题标题】:Loop optimisation techniques in C++C++ 中的循环优化技术
【发布时间】:2013-08-13 15:44:36
【问题描述】:

为了提高应用程序的性能,我们必须在开发阶段考虑循环优化技术

我想向您展示一些不同的方法来迭代一个简单的std::vector<uint32_t> v

  1. 带索引的未优化循环:

    uint64_t sum = 0;
    for (unsigned int i = 0; i < v.size(); i++)
        sum += v[i];
    
  2. 带有迭代器的未优化循环:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it;
    for (it = v.begin(); it != v.end(); it++)
        sum += *it;
    
  3. 缓存的std::vector::end迭代器:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it, end(v.end());
    for (it = v.begin(); it != end; it++)
        sum += *it;
    
  4. 预增量迭代器:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it, end(v.end());
    for (it = v.begin(); it != end; ++it)
        sum += *it;
    
  5. 基于范围的循环:

    uint64_t sum = 0;
    for (auto const &x : v)
        sum += x;
    

还有其他方法可以在 C++ 中构建循环;例如通过使用std::for_eachBOOST_FOREACH 等...

在您看来,提高性能的最佳方法是什么?为什么?

此外,在性能关键的应用程序中,展开循环可能很有用:同样,您会建议哪种方法?

【问题讨论】:

  • 具有良好优化的良好编译器应该为所有这些产生相同的程序集。如果您的编译器支持 restrict 关键字,您可以通过抓取向量的数组并在 for 循环中求和(更多的 C 方法)来获得更好的性能。你可能不会。查看优化下的程序集,看看是否有任何不同。
  • 过早优化是万恶之源。
  • @Snps 你怎么知道它为时过早? OP 很可能已经确定了一个关键瓶颈。不回答问题的老生常谈是没有用的。
  • @sfstewman “在开发阶段”——可能为时过早。
  • @Xaqq 取决于应用程序的类型。如果您正在编写模拟软件或游戏,开发通常具有性能标准。开发结束时的优化是正常的。

标签: c++ loops optimization vector iterator


【解决方案1】:

没有硬性规定,因为它取决于 执行。如果我几年前采取的措施是 然而,典型的:关于唯一有所作为的事情 正在缓存结束迭代器。修复前或修复后不会 区别,与容器和迭代器类型无关。

当时,我没有衡量索引(因为我在比较 不同类型容器的迭代器,而不是全部 支持索引)。但我猜如果你使用索引, 你也应该缓存v.size()的结果。

当然,这些措施是针对一个编译器 (g++) 的 系统,具有特定的硬件。你能知道的唯一方法 你的环境是衡量你自己的。

请注意:您确定已开启全面优化。 我的测量结果显示 3 和 4 之间没有区别,我怀疑 编译器今天优化较少。

对于这里的优化来说非常重要的是 函数实际上是内联的。如果他们不是, 后增量确实需要一些额外的复制,并且 通常需要一个额外的函数调用(到副本 迭代器的构造函数)也是如此。一旦函数 内联,然而,编译器可以很容易地看到这一切都是 一个无关紧要的,并且(至少在我尝试它时)完全生成 两种情况下的代码相同。 (我会使用预增量 反正。不是因为它有所作为,而是因为如果你 不要,尽管有些白痴会声称它会 你的措施。或者也许他们不是白痴,只是在使用 一个特别愚蠢的编译器。)

说实话,当我进行测量时,我很惊讶 缓存结束迭代器会有所不同,即使对于 向量,因为 pre- 和 后增量,甚至对于映射到地图的反向迭代器。 毕竟,end() 也被内联了;事实上,每一个 我的测试中使用的函数是内联的。

关于展开循环:我可能会这样做:

std::vector<uint32_t>::const_iterator current = v.begin();
std::vector<uint32_t>::const_iterator end = v.end();
switch ( (end - current) % 4 ) {
case 3:
    sum += *current ++;
case 2:
    sum += *current ++;
case 1:
    sum += *current ++;
case 0:
}
while ( current != end ) {
    sum += current[0] + current[1] + current[2] + current[3];
    current += 4;
}

(这是 4 的因数。如果 必要的。)

【讨论】:

  • 您使用了什么级别的优化? std::vectorsize 方法应该内联在 -O2 或更高版本,并且最新版本的 GCC 也可能能够将其提升到循环之外(有效地缓存它)。
  • @sfstewman 根据编译器文档,最大值。我没有做任何测试索引,所以我没有使用size(),但我希望同样的考虑适用于end()。不过,至少在当时,显式缓存 end() 确实让事情变得更快。
  • 非常好的展开技术 (+1),但我有一个问题,展开技术(如您所展示的)在现代 C++ 编译器中有效吗?
  • @MM。我不知道。我最近从不需要它们。我认为许多现代编译器可以自己找到它们。 (但是,我会发誓,当我做我的测量时,编译器会自动缓存end(),但他们没有。)
  • 实际上最好为整个循环设置 4 个单独的 sum 变量。然后仅在最后合并它们。这打破了中间令人讨厌的依赖链。
【解决方案2】:

现代编译器很可能会为您上面给出的方法生成相同的程序集。您应该查看实际的程序集(启用优化后)才能看到。

当您担心循环的速度时,您应该真正考虑一下您的算法是否真的是最优的。如果您确信它是,那么您需要考虑(并利用)数据结构的底层实现。 std::vector 在下面使用一个数组,并且根据编译器和函数中的其他代码,指针别名可能会阻止编译器完全优化您的代码。

有大量关于指针别名的信息(包括What is the strict aliasing rule?),但Mike Acton 有一些关于pointer aliasing 的精彩信息。

restrict 关键字(请参阅 What does the restrict keyword mean in C++?Mike Acton),多年来一直通过编译器扩展提供并在 C99 中编码(目前仅作为 C++ 中的编译器扩展提供),旨在处理这。在您的代码中使用它的方式更像 C,但可能允许编译器更好地优化您的循环,至少对于您给出的示例:

uint64_t sum = 0;
uint32_t *restrict velt = &v[0];
uint32_t *restrict vend = velt + v.size();
while(velt < vend) {
  sum += *velt;
  velt++;
}

但是,要查看这是否会产生差异,您确实需要针对您的实际问题分析不同的方法,并可能查看生成的底层程序集。如果您要对简单数据类型求和,这可能会对您有所帮助。如果您正在做任何更复杂的事情,包括调用无法在循环中内联的函数,则根本不可能有任何不同。

【讨论】:

    【解决方案3】:

    我假设您很清楚过早进行微优化的弊端,并且您已经通过分析和其他所有操作确定了代码中的热点。我还假设您只关心速度方面的性能。也就是说,您并不十分关心生成的代码的大小或内存使用情况。

    您提供的代码 sn-ps 将产生大致相同的结果,但缓存的 end() 迭代器除外。除了尽可能多地进行缓存和内联之外,您无法通过调整上述循环的结构来显着提高性能。

    在关键路径中编写高性能代码首先依赖于为工作选择最佳算法。如果您有性能问题,请先仔细查看算法。编译器在微优化您编写的代码方面通常会做得比您希望的要好得多。

    话虽如此,您可以做一些事情来为您的编译器提供一点帮助。

    • 尽可能缓存所有内容
    • 尽量减少小分配,尤其是在循环中
    • 尽可能多地制作const。这为编译器提供了额外的微优化机会。
    • 充分了解您的工具链并利用这些知识
    • 充分了解您的架构并利用这些知识
    • 学习阅读汇编代码并检查编译器的汇编输出

    学习您的工具链和架构将产生最大的好处。例如,GCC 有许多选项可以启用以提高性能,包括循环展开。见here。在迭代数据集时,保持每个项目与缓存行的大小对齐通常是有益的。在现代架构中,这通常意味着 64 字节,但请了解您的架构。

    Here 是在英特尔环境中编写高性能 C++ 的绝佳指南。

    一旦您了解了您的架构和工具链,您可能会发现您最初选择的算法在您的现实世界中并不是最优的。面对新数据,乐于改变。

    【讨论】:

    • 别忘了 Agner Fog 的优化手册:agner.org/optimize/#manuals
    • @sfstewman:哎呀!我的第二个链接应该指向那里,但我链接错误。已编辑。
    • “尽可能缓存所有内容”引入了冗余,这可能很糟糕。
    【解决方案4】:

    如果您使用的是 clang,则将这些标志传递给它:

      -Rpass-missed=loop-vectorize
      -Rpass-analysis=loop-vectorize
    

    在 Visual C++ 中,将此添加到构建中:

      /Qvec-report:2
    

    这些标志会告诉你循环是否无法矢量化(并给你一个经常神秘的信息来解释原因)。

    但总的来说,更喜欢选项 4 和 5(或 std::for_each)。虽然 clang 和 gcc 在大多数情况下通常会做得不错,但可悲的是,Visual C++ 倾向于谨慎行事。如果变量的范围未知(例如,传递给函数的引用或指针,或 this 指针),则向量化通常会失败(本地范围内的容器几乎总是会向量化)。

    #include <vector>
    #include <cmath>
    
    // fails to vectorise in Visual C++ and /O2
    void func1(std::vector<float>& v)
    {
      for(size_t i = 0; i < v.size(); ++i)
      {
        v[i] = std::sqrt(v[i]);
      }
    }
    
    // this will vectorise with sqrtps
    void func2(std::vector<float>& v)
    {
      for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it)
      {
        *it = std::sqrt(*it);
      }
    }
    

    Clang 和 gcc 也不能幸免于这些问题。如果您总是复制开始/结束,那么这不是问题。

    这是另一个可悲地影响许多编译器的经典之作(clang 3.5.0 未能通过这个微不足道的测试,但它已在 clang 4.0 中修复)。它突然出现了很多!

    struct Foo
    {
      void func3();
      void func4();
      std::vector<float> v;
      float f;
    };
    
    // will not vectorise
    void Foo::func3()
    {
      // this->v.end() !!
      for(std::vector<float>::iterator it = v.begin(); it != v.end(); ++it)
      {
        *it *= f;  // this->f !!
      }
    }
    
    void Foo::func4()
    {
      // you need to take a local copy of v.end(), and 'f'.
      const float temp = f;
      for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it)
      {
        *it *= temp;
      }
    }
    

    最后,如果这是您关心的问题,请使用编译器的矢量化报告来修复您的代码。如上所述,这基本上是指针别名的问题。您可以使用restrict 关键字来帮助解决其中一些问题(但我发现将restrict 应用于“this”通常没那么有用)。

    【讨论】:

      【解决方案5】:

      默认使用基于范围,因为它会给编译器最直接的优化信息(例如,编译器知道它可以缓存结束迭代器)。然后分析并仅在您发现重大瓶颈时进一步优化。在现实世界中,这些不同的循环变体会产生有意义的性能差异的情况很少。编译器非常擅长循环优化,您更有可能将优化工作集中在其他地方(例如选择更好的算法或专注于优化循环体)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-12-08
        • 1970-01-01
        • 1970-01-01
        • 2013-09-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多