【问题标题】:Will the compiler unroll this loop?编译器会展开这个循环吗?
【发布时间】:2011-09-02 18:13:20
【问题描述】:

我正在创建一个多维向量(数学向量),其中允许基本的数学运算 +、-、/、*、=。模板有两个参数,一个是类型(int、float 等),另一个是向量的大小。目前我正在通过 for 循环应用这些操作。现在考虑到在编译时知道大小,编译器会展开循环吗?如果没有,有没有办法在没有(或最小)性能损失的情况下展开它?

template <typename T, u32 size>
class Vector
{
public:
    // Various functions for mathematical operations. 
    // The functions take in a Vector<T, size>.
    // Example:
    void add(const Vector<T, size>& vec)
    {
        for (u32 i = 0; i < size; ++i)
        {
            values[i] += vec[i];
        }
    }

private:
    T   values[size];
};

在有人遇到Profile then optimize 之前,请注意这是我的 3D 图形引擎的基础,它必须很快。其次,为了自学,我想知道。

【问题讨论】:

  • 听起来不是很刻薄,但是如果您使用优化进行编译并转储程序集,您几乎可以找到:)
  • 即使它必须非常快,分析也是一个非常宝贵的工具。事实上,如果必须非常快,它是你的节日朋友尤其是,因为它比猜测要好得多。 (此外,答案可能在很大程度上取决于编译器、使用的标志,甚至更多。)
  • @larsman:如果您在 3-D 模型中有超过 40 亿个点,那么与使用 32 位计数相比,您遇到的问题更大。 (除此之外,GPU 驱动程序可能已经硬编码了 32 位计数。)
  • 这确实闻起来像是过早的优化。首先让您的图形引擎工作。然后分析和优化。我猜这段代码不会成为你的瓶颈。
  • @Ben Voigt 从不。 Dijkstra 不是说过称职的程序员“因此他以完全谦逊的态度处理他的任务,并避免像瘟疫一样的正确性”?... ;)

标签: c++ visual-studio-2008 compiler-optimization


【解决方案1】:

可以使用递归模板实例化展开循环。这在您的 C++ 实现中可能会更快,也可能不会更快。

我稍微调整了您的示例,以便它可以编译。

typedef unsigned u32; // or something similar

template <typename T, u32 size>
class Vector
{
  // need to use an inner class, because member templates of an 
  // unspecialized template cannot be explicitly specialized.
  template<typename Vec, u32 index>
  struct Inner
  {
    static void add(const Vec& a, const Vec& b)
    {
      a.values[index] = b.values[index];
      // triggers recursive instantiation of Inner 
      Inner<Vec, index-1>::add(a,b);
    }
  };
  // this specialization terminates the recursion
  template<typename Vec>
  struct Inner<Vec, 0>
  {
    static void add(const Vec& a, const Vec& b)
    {
      a.values[0] = b.values[0];
    }
  };

public:

    // PS! this function should probably take a 
    // _const_ Vector, because the argument is not modified
    // Various functions for mathematical operations. 
    // The functions take in a Vector<T, size>.
    // Example:
    void add(Vector<T, size>& vec)
    {
      Inner<Vector, size-1>::add(*this, vec);
    }

    T   values[size];
};

【讨论】:

    【解决方案2】:

    您可以通过反汇编执行以下技巧,以查看特定代码的编译方式。

        Vector<int, 16> a, b;
        Vector<int, 65536> c, d;
    
        asm("xxx"); // marker
        a.Add(b);
        asm("yyy"); // marker
        c.Add(d);
        asm("zzz"); // marker
    

    现在编译

    gcc -O3 1.cc -S -o 1.s
    

    然后看到disasm

        xxx
    # 0 "" 2
    #NO_APP
        movdqa  524248(%rsp), %xmm0
        leaq    524248(%rsp), %rsi
        paddd   524184(%rsp), %xmm0
        movdqa  %xmm0, 524248(%rsp)
        movdqa  524264(%rsp), %xmm0
        paddd   524200(%rsp), %xmm0
        movdqa  %xmm0, 524264(%rsp)
        movdqa  524280(%rsp), %xmm0
        paddd   524216(%rsp), %xmm0
        movdqa  %xmm0, 524280(%rsp)
        movdqa  524296(%rsp), %xmm0
        paddd   524232(%rsp), %xmm0
        movdqa  %xmm0, 524296(%rsp)
    #APP
    # 36 "1.cc" 1
        yyy
    # 0 "" 2
    #NO_APP
        leaq    262040(%rsp), %rdx
        leaq    -104(%rsp), %rcx
        xorl    %eax, %eax
        .p2align 4,,10
        .p2align 3
    .L2:
        movdqa  (%rcx,%rax), %xmm0
        paddd   (%rdx,%rax), %xmm0
        movdqa  %xmm0, (%rdx,%rax)
        addq    $16, %rax
        cmpq    $262144, %rax
        jne .L2
    #APP
    # 38 "1.cc" 1
        zzz
    

    如您所见,第一个循环小到可以展开。第二个是循环。

    【讨论】:

      【解决方案3】:

      首先,完全不确定展开循环是否有益。

      您问题的唯一可能答案是“取决于”(取决于编译器标志、size 的值等)。

      如果您真的想知道,请询问您的编译器:使用典型值 size 和您真正使用的优化标志编译成汇编代码,然后检查结果。

      【讨论】:

        【解决方案4】:

        解决这个问题的唯一方法是在您自己的编译器上使用您自己的优化参数进行尝试。使用您的“是否展开”代码制作一个测试文件,test.cpp:

        #include "myclass.hpp"
        
        void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
            a.add( b );
        }
        

        然后是参考代码 sn -p reference.cpp:

        #include "myclass.hpp"
        
        void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
            a[0] += b[0];
            a[1] += b[1];
            a[2] += b[2];
        }
        

        现在使用 GCC 编译它们并只输出程序集:

        for x in *.cpp; do  g++ -c "$x" -Wall -Wextra -O2 -S -o "out/$x.s"; done
        

        根据我的经验,当使用在编译时已知持续时间的循环时,GCC 将默认展开 3 个或更少的循环;使用-funroll-loops 会使其展开更多。

        【讨论】:

          【解决方案5】:

          首先:现代 CPU 在预测分支方面非常聪明,因此展开循环可能无济于事(甚至可能有害)。

          第二:是的,现代编译器知道如何展开这样的循环,如果这对您的目标 CPU 来说是个好主意。

          第三:现代编译器甚至可以自动向量化循环,这甚至比展开更好。

          底线:不要认为自己比编译器更聪明,除非您对 CPU 架构了解很多。以简单、直接的方式编写代码,在分析器告诉您之前不要担心微优化。

          【讨论】:

            【解决方案6】:

            许多编译器会展开这个循环,不知道您所指的“编译器”是否会展开。世界上不只有一个编译器。

            如果您想保证它是展开的,那么 TMP(带内联)可以做到这一点。 (这实际上是 TMP 中比较琐碎的应用之一,通常用作元编程的示例)。

            【讨论】:

            • 我已经添加了相关标签。它是 MSVC 编译器。
            • @Samarusa:没有“编译器”。你知道有多少种 MSVC 吗?将版本字符串(运行 cl.exe /version 时由编译器打印)添加到您的问题中。
            猜你喜欢
            • 2017-01-27
            • 1970-01-01
            • 2013-04-07
            • 1970-01-01
            • 2016-09-17
            • 1970-01-01
            • 1970-01-01
            • 2021-10-04
            • 2011-10-30
            相关资源
            最近更新 更多