【问题标题】:More aggresive optimization for FMA operations对 FMA 操作进行更积极的优化
【发布时间】:2021-02-17 06:39:45
【问题描述】:

我想构建一个表示多个(比如N)算术类型的数据类型,并使用运算符重载提供与算术类型相同的接口,这样我就得到了一个像 Agner Fog 的vectorclass 这样的数据类型。

请看这个例子:Godbolt

#include <array>

using std::size_t;

template<class T, size_t S>
class LoopSIMD : std::array<T,S>
{
public:
    friend LoopSIMD operator*(const T a, const LoopSIMD& x){
        LoopSIMD result;
        for(size_t i=0;i<S;++i)
            result[i] = a*x[i];
        return result;
    }

    LoopSIMD& operator +=(const LoopSIMD& x){
        for(size_t i=0;i<S;++i){
            (*this)[i] += x[i];
        }
        return *this;
    }
};

constexpr size_t N = 7;
typedef LoopSIMD<double,N> SIMD;

SIMD foo(double a, SIMD x, SIMD y){
    x += a*y;
    return x;
}

对于一定数量的元素,这似乎工作得很好,gcc-10 为 6,clang-11 为 27。对于大量元素,编译器不再使用 FMA(例如vfmadd213pd)操作。相反,它们分别进行乘法(例如vmulpd)和加法(例如vaddpd)。

问题:

  • 这种行为有充分的理由吗?
  • 是否有任何编译器标志,以便我可以将上述值 6 用于 gcc,将 27 用于 clang?

谢谢!

【问题讨论】:

    标签: c++ gcc clang fma


    【解决方案1】:

    我发现给定示例的改进。

    在循环之前添加#pragma omp simd GCC 设法使 FMA 优化达到N=71

    https://godbolt.org/z/Y3T1rs37W

    如果使用 AVX512,尺寸会更大:

    https://godbolt.org/z/jWWPP7W5G

    【讨论】:

    • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
    【解决方案2】:

    对于 gcc 10.2,我执行了以下操作,并获得了一些相当不错的结果,其中 -Ofast -march=skylake -ffast-math 与您的 godbolt 链接相同。

    friend LoopSIMD operator*(const T a, const LoopSIMD& x) {
        LoopSIMD result;
        std::transform(x.cbegin(), x.cend(), result.begin(),
                       [a](auto const& i) { return a * i; });
        return result;
    }
    
    LoopSIMD& operator+=(const LoopSIMD& x) {
        std::transform(this->cbegin(), this->cend(), x.cbegin(), this->begin(),
                       [](auto const& a, auto const& b) { return a + b; });
        return *this;
    }
    

    std::transform 有一些疯狂的重载,所以我想我需要解释一下。

    第一个重载捕获a,将每个值相乘,并将其存储回结果的开头。

    第二个重载充当zip,将xthis 中的两个值相加,并将结果存储回this

    如果您没有与operator+=operator* 结婚,您可以像这样创建自己的fma

        LoopSIMD& fma(const LoopSIMD& x, double a ){
            std::transform_inclusive_scan(
                x.cbegin(),
                x.cend(),
                this->begin(),
                std::plus{},
                [a](auto const& i){return i * a;},
                0.0);
            return *this;
        }
    

    这需要 c++17,但会循环保留 SIMD 指令

    foo(double, LoopSIMD<double, 40ul>&, LoopSIMD<double, 40ul> const&):
            xor     eax, eax
            vxorpd  xmm1, xmm1, xmm1
    .L2:
            vfmadd231sd     xmm1, xmm0, QWORD PTR [rsi+rax]
            vmovsd  QWORD PTR [rdi+rax], xmm1
            add     rax, 8
            cmp     rax, 320
            jne     .L2
            ret
    

    【讨论】:

    • 谢谢!但是您的代码中有错误。操作员调用中的std::transform 调用必须是std::transform(x.begin(), x.end(), result.begin(), ...);。然后我得到了与我的示例相同的结果。
    • 啊,很好。我也忘记添加 cbegin 和 cend 迭代器,虽然它似乎确实有所改进,但核心功能似乎没有太大变化。您可能可以使用惰性求值、函数式编程和内在函数来做一些事情,这可能会改善事情,但我认为这需要更多的工作
    • 别忘了将y 设为常量引用,你会看到 foo 也有相当大的减少。也许可以创建x 的副本并将其设为 const ref 也会有所帮助
    【解决方案3】:

    您也可以简单地制作自己的 fma 函数:

    template<class T, size_t S>
    class LoopSIMD : std::array<T,S>
    {
    public:
        friend LoopSIMD fma(const LoopSIMD& x, const T y, const LoopSIMD& z) {
            LoopSIMD result;
            for (size_t i = 0; i < S; ++i) {
                result[i] = std::fma(x[i], y, z[i]);
            }
            return result;
        }
        friend LoopSIMD fma(const T y, const LoopSIMD& x, const LoopSIMD& z) {
            LoopSIMD result;
            for (size_t i = 0; i < S; ++i) {
                result[i] = std::fma(y, x[i], z[i]);
            }
            return result;
        }
        // And more variants, taking `const LoopSIMD&, const LoopSIMD&, const T`, `const LoopSIMD&, const T, const T`, etc
    };
    
    SIMD foo(double a, SIMD x, SIMD y){
        return fma(a, y, x);
    }
    

    但首先要进行更好的优化,您应该对齐阵列。如果你这样做,你的原始代码优化得很好:

    constexpr size_t next_power_of_2_not_less_than(size_t n) {
        size_t pow = 1;
        while (pow < n) pow *= 2;
        return pow;
    }
    
    template<class T, size_t S>
    class LoopSIMD : std::array<T,S>
    {
    public:
        // operators
    } __attribute__((aligned(next_power_of_2_not_less_than(sizeof(T[S])))));
    
    // Or with a c++11 attribute
    /*
    template<class T, size_t S>
    class [[gnu::aligned(next_power_of_2_not_less_than(sizeof(T[S])))]] LoopSIMD : std::array<T,S>
    {
    public:
        // operators
    };
    */
    
    SIMD foo(double a, SIMD x, SIMD y){
        x += a * y;
        return x;
    }
    

    【讨论】:

    • 谢谢!我知道如何用 c++ 技术解决这个问题。我认为另一种选择是由operator* 返回的表达式模板,然后在operator += 中应用fma 操作。但我的目标更多是针对问题中的编译器优化,因为我们已经运行了我不想更改的代码。
    猜你喜欢
    • 2020-03-02
    • 2021-10-26
    • 1970-01-01
    • 2013-04-12
    • 2020-07-25
    • 1970-01-01
    • 2020-07-24
    • 2016-06-11
    • 2016-01-24
    相关资源
    最近更新 更多