これは、 GCCをループで最適化することに関するこの質問のフォローアップです。memcpy()
私はあきらめて、ループを手動で最適化する直接ルートに行くことにしました。
ただし、可能な限り移植性と保守性を維持しようとしているので、SSE組み込み関数に頼ることなく、GCCにループ内の単純な最適化された繰り返しコピー自体をベクトル化させたいと考えています。MOVDQA
ただし、手動でベクトル化されたバージョン(SSE2命令を使用)は、小さな配列(<32要素)で経験的に最大58%高速であり、少なくとも17であるにもかかわらず、手持ちの量に関係なく、そうすることを拒否しているようです。大きいものの場合は%速くなります(> = 512)。
手動でベクトル化されていないバージョンは次のとおりです(GCCにベクトル化するように指示するために考えられる限り多くのヒントがあります)。
__attribute__ ((noinline))
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, j, l;
double * __restrict__ subout __attribute__ ((aligned (16)));
double * __restrict__ subin __attribute__ ((aligned (16)));
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
l = k - (k % 8);
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout = &out[i * stride_out_0];
subin = &in[idx * stride_in_0];
for(j = 0; j < l; j += 8) {
subout[j+0] = subin[j+0];
subout[j+1] = subin[j+1];
subout[j+2] = subin[j+2];
subout[j+3] = subin[j+3];
subout[j+4] = subin[j+4];
subout[j+5] = subin[j+5];
subout[j+6] = subin[j+6];
subout[j+7] = subin[j+7];
}
for( ; j < k; ++j)
subout[j] = subin[j];
}
}
そして、これがパフォーマンスの比較に使用した手動ベクトル化の最初の試みです(これは間違いなくさらに改善される可能性がありますが、可能な限り最も単純な変換をテストしたかっただけです)。
__attribute__ ((noinline))
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, j, l;
__m128i * __restrict__ subout1 __attribute__ ((aligned (16)));
__m128i * __restrict__ subin1 __attribute__ ((aligned (16)));
double * __restrict__ subout2 __attribute__ ((aligned (16)));
double * __restrict__ subin2 __attribute__ ((aligned (16)));
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
l = (k - (k % 8)) / 2;
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout1 = (__m128i*)&out[i * stride_out_0];
subin1 = (__m128i*)&in[idx * stride_in_0];
for(j = 0; j < l; j += 4) {
subout1[j+0] = subin1[j+0];
subout1[j+1] = subin1[j+1];
subout1[j+2] = subin1[j+2];
subout1[j+3] = subin1[j+3];
}
j *= 2;
subout2 = &out[i * stride_out_0];
subin2 = &in[idx * stride_in_0];
for( ; j < k; ++j)
subout2[j] = subin2[j];
}
}
(実際のコードは、いくつかの特殊なケースを処理するために少し複雑ですが、上記のバージョンでもベクトル化されないため、GCCベクトル化に影響を与える方法ではありません:私のテストハーネスはLiveWorkspaceにあります)
次のコマンドラインを使用して最初のバージョンをコンパイルしています。
gcc-4.7 -O3 -ftree-vectorizer-verbose=3 -march=pentium4m -fverbose-asm \
-msse -msse2 -msse3 take.c -DTAKE5 -S -o take5.s
また、メインコピーループに使用される結果の命令は、SSE組み込み関数を手動で使用した場合に生じる命令ではなく、常にFLDL
/ペア(つまり、8バイト単位でコピー)になります。FSTPL
MOVDQA
からの関連する出力は次のtree-vectorize-verbose
ようです。
take.c:168でのループの分析
168:vect_model_store_cost:ハードウェアでサポートされている非整列。
168:vect_model_store_cost:inside_cost = 8、outside_cost=0。
168:vect_model_load_cost:ハードウェアでサポートされている非整列。
168:vect_model_load_cost:inside_cost = 8、outside_cost=0。
168:コストモデル:ループバージョニングエイリアシングのチェックのコストを追加します。
168:コストモデル:ループの反復が不明であるため、エピローグピールイターがvf/2に設定されています。
168:コストモデル:ベクトル反復コスト=16をスカラー反復コスト=16で割った値は、ベクトル化係数= 1以上です
。168:ベクトル化されていない:ベクトル化は収益性がありません。
なぜそれが「整列されていない」ストアとロードを指しているのかわかりません。いずれにせよ、問題は、ベクトル化が有益であると証明できないことであるように思われます(経験的に、それはすべての場合に重要であり、どうなるかわかりません)。
私がここで見逃している単純なフラグやヒントはありますか、それともGCCはこれを何があってもやりたくないのですか?
これが明らかなことなら恥ずかしいことですが、もしそうなら、これが他の誰かにも役立つことを願っています。