针对FMA操作的更具攻击性的优化



我想构建一个表示多个(比如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(。

问题:

  • 这种行为有充分的理由吗
  • 有没有编译器标志可以让我增加上面提到的gcc的值6和clang的值27

谢谢!

您也可以简单地创建自己的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;
}

我做了以下操作,并获得了一些非常好的结果,对于具有与godbolt链接相同的-Ofast -march=skylake -ffast-math的gcc 10.2。

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

我发现了对给定示例的改进。

在循环GCC之前添加CCD_ 15使得FMA优化达到CCD_。

https://godbolt.org/z/Y3T1rs37W

如果使用AVX512,尺寸可能会得到更大的改进:

https://godbolt.org/z/jWWPP7W5G

最新更新