GCC不能像C数组一样优化对齐的std :: array。

25

这是一段使用std::array时,GCC 6和7无法进行优化的代码:

#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数组不会产生运行时成本的假设。
有什么简单的东西我错过了可以解决这个问题吗?到目前为止,我想出了一个丑陋的hack:
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


4
值得一提的是,clang指出alignas必须放在数据成员上而不是typedef上,但如果将Vec更改为一个嵌套类,其中包含一个作为对齐数据成员的std::array<...>,并给它加上operator[]重载,那么clang确实能够进行优化。GCC仍不能。 - user743382
3
std::array底层数组的对齐方式和std::array本身的对齐方式相同吗? - user2486888
3
很明显是编译器的一个漏洞。如果你想解决这个问题,你应该通过 Bugzilla 报告它。 - rustyx
4
@RustyX说:虽然我希望GCC有一天能够修复这个问题,但我的问题是:“我是否错过了什么简单的解决方法?”换句话说,我想要一个相对不显眼的解决方法,使std::array在GCC 6上实现最佳性能。我不想只是为GCC 8而等待。 - John Zwinck
5
我已经在这里报告了:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80561。 - John Zwinck
显示剩余5条评论
1个回答

18

有趣的是,如果您用v1._M_elems[i] += v2._M_elems[i];替换v1[i] += v2a[i];(这显然不可移植),gcc可以像处理C数组一样优化std::array情况。

可能的解释是:在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这样不需要对齐地址的指令。

您发现的技巧,即将其转换为Vec,是另一种让编译器看到处理内存访问时正在处理的类型已对齐的方法。对于常规的std::array :: operator [],内存访问发生在std :: array的成员函数中,该函数并不知道*this恰好对齐。

gcc还有一种内置的方法可以让编译器了解关于对齐方式的信息:

const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));

3
非常感谢您提交了错误报告 :-) - Marc Glisse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接