私のプログラムには、単純なベクトル加算を行う関数がありますc[0:15] = a[0:15] + b[0:15]
。関数プロトタイプは次のとおりです。
void vecadd(float * restrict a, float * restrict b, float * restrict c);
32ビットの組み込みアーキテクチャには、次のようなダブルワードをロード/保存するロード/ストアオプションがあります。
r16 = 0x4000 ;
strd r0,[r16] ; stores r0 in [0x4000] and r1 in [0x4004]
GCCオプティマイザーは、ループのベクトルの性質を認識し、コードの2つのブランチを生成します。1つは3つの配列がダブルワードアラインされている場合(つまり、ダブルロード/ストア命令を使用する場合)、もう1つは配列がダブルロード/ストア命令を使用する場合です。ワードアラインされています(単一のロード/ストアオプションを使用します)。
問題は、アドレスアラインメントチェックが加算部分に比べてコストがかかることです。コンパイラにa、b、cが常に8アラインされることをほのめかして、それを排除したいと思います。これをコンパイラに伝えるためにポインタ宣言に追加する修飾子はありますか?
この関数の呼び出しに使用される配列にはaligned(8)属性がありますが、関数コード自体には反映されていません。この属性を関数パラメーターに追加することは可能ですか?
私のシステムで見つけたサンプルコードの一部に続いて、私は次の解決策を試しました。これには、前に示したいくつかの回答からのアイデアが組み込まれています。基本的に、64ビットタイプのfloatの小さな配列の和集合を作成します-この場合、floatのSIMDベクトル-そしてオペランドfloat配列のキャストを使用して関数を呼び出します。
_typedef float f2 __attribute__((vector_size(8)));
typedef union { f2 v; float f[2]; } simdfu;
void vecadd(f2 * restrict a, f2 * restrict b, f2 * restrict c);
float a[16] __attribute__((aligned(8)));
float b[16] __attribute__((aligned(8)));
float c[16] __attribute__((aligned(8)));
int main()
{
vecadd((f2 *) a, (f2 *) b, (f2 *) c);
return 0;
}
_
これで、コンパイラは4整列ブランチを生成しません。
ただし、__builtin_assume_aligned()
が望ましい解決策であり、それが機能するだけの場合は、キャストや起こりうる副作用を防ぎます。
編集:組み込み関数は実際には実装にバグがあることに気づきました(つまり、機能しないだけでなく、コードの後半で計算エラーが発生します。
属性が機能しない場合、またはオプションではない場合..。
よくわかりませんが、これを試してください。
void vecadd (float * restrict a, float * restrict b, float * restrict c)
{
a = __builtin_assume_aligned (a, 8);
b = __builtin_assume_aligned (b, 8);
c = __builtin_assume_aligned (c, 8);
for ....
これにより、ポインタが整列していることがGCCに通知されます。それから、それがあなたが望むことをするかどうかは、コンパイラがその情報を効果的に使用できるかどうかに依存します。十分に賢くないかもしれません:これらの最適化は簡単ではありません。
別のオプションは、8バイトで整列する必要があるユニオン内にfloatをラップすることです。
typedef union {
float f;
long long dummy;
} aligned_float;
void vedadd (aligned_float * a, ......
これで8バイトのアラインメントが強制されるはずだと思いますが、コンパイラーがそれを使用するのに十分賢いかどうかはわかりません。
ポインタ引数が常にダブルワード整列であることをGCCに伝えるにはどうすればよいですか?
GCCの新しいバージョンには ___builtin_assume_aligned
_ があるようです:
組み込み関数:
void * __builtin_assume_aligned (const void *exp, size_t align, ...)
この関数は最初の引数を返し、コンパイラーは、返されたポインターが少なくとも整列バイト整列されていると想定できるようにします。このビルトインは2つまたは3つの引数を持つことができ、3つある場合、3番目の引数は整数型である必要があり、ゼロ以外の場合はミスアライメントオフセットを意味します。例えば:
_void *x = __builtin_assume_aligned (arg, 16);
_これは、コンパイラがargに設定されたxが少なくとも16バイトに整列していると想定できることを意味します。
_void *x = __builtin_assume_aligned (arg, 32, 8);
_これは、コンパイラがargに設定されたxについて、(char *)x-8が32バイトに整列されていると想定できることを意味します。
2010年頃のStackOverflowに関する他のいくつかの質問と回答に基づくと、組み込みはGCC3と初期のGCC4では利用できなかったようです。しかし、カットオフポイントがどこにあるのかわかりません。
gccバージョンは、単純なtypedefと配列のalign()については危険です。通常、必要なことを行うには、floatを構造体でラップし、含まれているfloatに配置制限を設定する必要があります。
演算子のオーバーロードを使用すると、これをほとんど苦痛なくすることができますが、C++構文を使用できることを前提としています。
#include <stdio.h>
#include <string.h>
#define restrict __restrict__
typedef float oldfloat8 __attribute__ ((aligned(8)));
struct float8
{
float f __attribute__ ((aligned(8)));
float8 &operator=(float _f) { f = _f; return *this; }
float8 &operator=(double _f) { f = _f; return *this; }
float8 &operator=(int _f) { f = _f; return *this; }
operator float() { return f; }
};
int Myfunc(float8 * restrict a, float8 * restrict b, float8 * restrict c);
int MyFunc(float8 * restrict a, float8 * restrict b, float8 * restrict c)
{
return *c = *a* *b;
}
int main(int argc, char **argv)
{
float8 a, b, c;
float8 p[4];
printf("sizeof(oldfloat8) == %d\n", (int)sizeof(oldfloat8));
printf("sizeof(float8) == %d\n", (int)sizeof(float8));
printf("addr p[0] == %p\n", &p[0] );
printf("addr p[1] == %p\n", &p[1] );
a = 2.0;
b = 7.0;
MyFunc( &a, &b, &c );
return 0;
}
配置仕様は通常、ポインターの基本タイプよりも小さい配置でのみ機能し、大きくは機能しません。
最も簡単なのは、配列全体をアライメント仕様で宣言することです。
typedef float myvector[16];
typedef myvector alignedVector __attribute__((aligned (8));
(構文が正しくない可能性があります。これらの__attribute__
sをどこに配置するかを常に知るのは困難です)
そして、コード全体でそのタイプを使用します。あなたの関数定義のために私は試してみます
void vecadd(alignedVector * restrict a, alignedVector * restrict b, alignedVector * restrict c);
これにより、追加の間接参照が提供されますが、これは構文にすぎません。 *a
のようなものは単なるヌープであり、ポインターを最初の要素へのポインターとして再解釈するだけです。