【问题标题】:GCC fails to optimize aligned std::array like C arrayGCC 无法像 C 数组一样优化对齐的 std::array
【发布时间】:2017-09-24 21:40:15
【问题描述】:

以下是 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 数组不会产生运行时成本的假设。

有没有什么简单的东西可以解决这个问题?到目前为止,我想出了一个丑陋的 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

【问题讨论】:

  • FWIW,clang 抱怨 alignas 需要在数据成员上,而不是在 typedef 上,但如果将 Vec 更改为将 std::array&lt;...&gt; 作为对齐数据成员的嵌套类,并且给它operator[] 重载,然后clang 确实设法优化了它。 GCC 仍然没有。
  • std::array 下的数组是否与std::array 具有相同的对齐方式?
  • 所以,显然是一个编译器错误。如果你想解决它,你应该通过 bugzilla 报告它。
  • @RustyX:虽然我希望 GCC 有朝一日能解决这个问题,但我在这里提出的问题是:我是否缺少一些简单的东西可以解决这个问题?换句话说,我想要一个相对不显眼的解决方法,它可以在 GCC 6 上实现 std::array 的最佳性能。我不想简单地为 GCC 8 屏住呼吸。
  • @RustyX:我在这里报告过:gcc.gnu.org/bugzilla/show_bug.cgi?id=80561

标签: c++ gcc optimization simd memory-alignment


【解决方案1】:

有趣的是,如果将 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 &amp;)v2_7(D) clique 1 base 1][_1]。也就是说,在第二种情况下,gcc 可能忘记了 this 是 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));

【讨论】:

  • 非常感谢您提交错误报告 :-)
猜你喜欢
  • 1970-01-01
  • 2012-01-05
  • 2018-10-07
  • 2020-12-23
  • 2010-09-18
  • 2015-12-11
  • 2013-06-25
  • 1970-01-01
相关资源
最近更新 更多