web-dev-qa-db-ja.com

GCCはC配列のように整列されたstd :: 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でコンパイルすると、Niceコードが生成されます。

    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ビットレジスタを介して一度に4つのdoubleを追加する2つのアンロールされた反復です。しかし、-DC_ARRAYなしでコンパイルすると、次のように大きな混乱が生じます。

    mov     rax, rdi
    shr     rax, 3
    neg     rax
    and     eax, 3
    je      .L7

この場合に生成されたコード(プレーンなC配列の代わりにstd::arrayを使用)は、typedefで32バイトにアライメントされていると指定されている場合でも、入力アレイのアライメントをチェックするようです。

GCCはstd::arrayの内容がstd::array自体と同じように配置されていることを理解していないようです。これは、C配列の代わりにstd::arrayを使用してもランタイムコストが発生しないという仮定を破ります。

これを修正するために欠けている簡単なものはありますか?これまでのところ、醜いハックを思いつきました。

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が8ではなく4の場合、問題は発生しません。 Clangを使用する場合、問題は発生しません。

あなたはそれをここでライブで見ることができます: https://godbolt.org/g/IXIOst

25
John Zwinck

興味深いことに、_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]を参照できます。つまり、2番目のケースでは、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));
_
18
Marc Glisse