GCC 无法像 C 数组那样优化对齐的 std::array
GCC fails to optimize aligned std::array like C array
这是 GCC 6 和 7 在使用 std::array
时无法优化的一些代码:
#include <array>
static constexpr size_t my_elements = 8;
class Foo
{
public:
#ifdef C_ARRAY
typedef double Vec[my_elements] alignas(32);
#else
typedef std::array<double, my_elements> Vec alignas(32);
#endif
void fun1(const Vec&);
Vec v1{{}};
};
void Foo::fun1(const Vec& __restrict__ v2)
{
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2[i];
}
}
用 g++ -std=c++14 -O3 -march=haswell -S -DC_ARRAY
编译上面的代码会生成漂亮的代码:
vmovapd ymm0, YMMWORD PTR [rdi]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi]
vmovapd YMMWORD PTR [rdi], ymm0
vmovapd ymm0, YMMWORD PTR [rdi+32]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi+32]
vmovapd YMMWORD PTR [rdi+32], ymm0
vzeroupper
这基本上是通过 256 位寄存器一次添加四个双精度数的两次展开迭代。但是,如果你在没有 -DC_ARRAY
的情况下进行编译,你会从这个开始变得一团糟:
mov rax, rdi
shr rax, 3
neg rax
and eax, 3
je .L7
在这种情况下生成的代码(使用 std::array
而不是普通的 C 数组)似乎检查输入数组的对齐方式——即使它在 typedef 中指定为对齐到 32 字节。
GCC 似乎不理解 std::array
的内容与 std::array
本身的对齐方式相同。这打破了使用 std::array
而不是 C 数组不会产生运行时成本的假设。
有什么我遗漏的简单方法可以解决这个问题吗?到目前为止,我想出了一个丑陋的技巧:
void Foo::fun2(const Vec& __restrict__ v2)
{
typedef double V2 alignas(Foo::Vec);
const V2* v2a = static_cast<const V2*>(&v2[0]);
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2a[i];
}
}
另请注意:如果 my_elements
是 4 而不是 8,则不会出现此问题。如果使用 Clang,则不会出现此问题。
你可以在这里看到:https://godbolt.org/g/IXIOst
有趣的是,如果将 v1[i] += v2a[i];
替换为 v1._M_elems[i] += v2._M_elems[i];
(这显然不可移植),gcc 会设法优化 std::array 的情况以及 C 数组的情况。
可能的解释:在 gcc 转储 (-fdump-tree-all-all
) 中,可以看到 C 数组情况下的 MEM[(struct FooD.25826 *)this_7(D) clique 1 base 0].v1D.25832[i_15]
,以及 std::array 的 MEM[(const value_typeD.25834 &)v2_7(D) clique 1 base 1][_1]
。也就是说,在第二种情况下,gcc 可能忘记了这是 Foo 类型的一部分,只记得它正在访问一个 double。
这是一种抽象惩罚,来自必须经过所有内联函数才能最终看到数组访问。 Clang 仍然设法很好地矢量化(即使在删除 alignas 之后!)。这可能意味着 clang 在不关心对齐的情况下进行矢量化,并且实际上它使用了像 vmovupd
这样不需要对齐地址的指令。
你发现的 hack,转换为 Vec,是另一种让编译器看到的方法,当它处理内存访问时,正在处理的类型是对齐的。对于常规 std::array::operator[],内存访问发生在 std::array 的成员函数内部,它没有任何线索表明 *this
恰好对齐。
Gcc 也有一个内置函数让编译器知道对齐:
const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));
这是 GCC 6 和 7 在使用 std::array
时无法优化的一些代码:
#include <array>
static constexpr size_t my_elements = 8;
class Foo
{
public:
#ifdef C_ARRAY
typedef double Vec[my_elements] alignas(32);
#else
typedef std::array<double, my_elements> Vec alignas(32);
#endif
void fun1(const Vec&);
Vec v1{{}};
};
void Foo::fun1(const Vec& __restrict__ v2)
{
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2[i];
}
}
用 g++ -std=c++14 -O3 -march=haswell -S -DC_ARRAY
编译上面的代码会生成漂亮的代码:
vmovapd ymm0, YMMWORD PTR [rdi]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi]
vmovapd YMMWORD PTR [rdi], ymm0
vmovapd ymm0, YMMWORD PTR [rdi+32]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi+32]
vmovapd YMMWORD PTR [rdi+32], ymm0
vzeroupper
这基本上是通过 256 位寄存器一次添加四个双精度数的两次展开迭代。但是,如果你在没有 -DC_ARRAY
的情况下进行编译,你会从这个开始变得一团糟:
mov rax, rdi
shr rax, 3
neg rax
and eax, 3
je .L7
在这种情况下生成的代码(使用 std::array
而不是普通的 C 数组)似乎检查输入数组的对齐方式——即使它在 typedef 中指定为对齐到 32 字节。
GCC 似乎不理解 std::array
的内容与 std::array
本身的对齐方式相同。这打破了使用 std::array
而不是 C 数组不会产生运行时成本的假设。
有什么我遗漏的简单方法可以解决这个问题吗?到目前为止,我想出了一个丑陋的技巧:
void Foo::fun2(const Vec& __restrict__ v2)
{
typedef double V2 alignas(Foo::Vec);
const V2* v2a = static_cast<const V2*>(&v2[0]);
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2a[i];
}
}
另请注意:如果 my_elements
是 4 而不是 8,则不会出现此问题。如果使用 Clang,则不会出现此问题。
你可以在这里看到:https://godbolt.org/g/IXIOst
有趣的是,如果将 v1[i] += v2a[i];
替换为 v1._M_elems[i] += v2._M_elems[i];
(这显然不可移植),gcc 会设法优化 std::array 的情况以及 C 数组的情况。
可能的解释:在 gcc 转储 (-fdump-tree-all-all
) 中,可以看到 C 数组情况下的 MEM[(struct FooD.25826 *)this_7(D) clique 1 base 0].v1D.25832[i_15]
,以及 std::array 的 MEM[(const value_typeD.25834 &)v2_7(D) clique 1 base 1][_1]
。也就是说,在第二种情况下,gcc 可能忘记了这是 Foo 类型的一部分,只记得它正在访问一个 double。
这是一种抽象惩罚,来自必须经过所有内联函数才能最终看到数组访问。 Clang 仍然设法很好地矢量化(即使在删除 alignas 之后!)。这可能意味着 clang 在不关心对齐的情况下进行矢量化,并且实际上它使用了像 vmovupd
这样不需要对齐地址的指令。
你发现的 hack,转换为 Vec,是另一种让编译器看到的方法,当它处理内存访问时,正在处理的类型是对齐的。对于常规 std::array::operator[],内存访问发生在 std::array 的成员函数内部,它没有任何线索表明 *this
恰好对齐。
Gcc 也有一个内置函数让编译器知道对齐:
const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));