以下は、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
興味深いことに、_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));
_