10

SSE4ドット積でこのコードを改善しようとしていますが、解決策を見つけるのに苦労しています。この関数は、それぞれ80セルのfloat配列を含むパラメーターqiおよびtjを取得し、内積を計算します。戻り値は、4つの内積を持つベクトルです。だから私がやろうとしているのは、20個の値の4つの内積を並行して計算することです。

このコードを改善する方法を知っていますか?

inline __m128 ScalarProd20Vec(__m128* qi, __m128* tj)
{
    __m128 res=_mm_add_ps(_mm_mul_ps(tj[0],qi[0]),_mm_mul_ps(tj[1],qi[1]));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[2],qi[2]),_mm_mul_ps(tj[3],qi[3])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[4],qi[4]),_mm_mul_ps(tj[5],qi[5])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[6],qi[6]),_mm_mul_ps(tj[7],qi[7])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[8],qi[8]),_mm_mul_ps(tj[9],qi[9])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));
    return res;
}
4

3 に答える 3

9

私がSOで見た何百ものSSEの例の中で、あなたのコードは最初からすでにかなり良い形になっている数少ないものの1つです。SSE4ドット積命令は必要ありません。(あなたはもっとうまくやることができます!)

ただし、試すことができることが1つあります(まだ時間を計っていないので、試してみてください)。

現在、にデータ依存チェーンがありresます。今日のほとんどのマシンでは、ベクトルの加算は3〜4サイクルです。したがって、次のようになっているため、コードの実行には最低30サイクルかかります。

(10 additions on critical path) * (3 cycles addps latency) = 30 cycles

できることはres、次のように変数をノード分割することです。

__m128 res0 = _mm_add_ps(_mm_mul_ps(tj[ 0],qi[ 0]),_mm_mul_ps(tj[ 1],qi[ 1]));
__m128 res1 = _mm_add_ps(_mm_mul_ps(tj[ 2],qi[ 2]),_mm_mul_ps(tj[ 3],qi[ 3]));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 4],qi[ 4]),_mm_mul_ps(tj[ 5],qi[ 5]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[ 6],qi[ 6]),_mm_mul_ps(tj[ 7],qi[ 7])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 8],qi[ 8]),_mm_mul_ps(tj[ 9],qi[ 9])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));

return _mm_add_ps(res0,res1);

これにより、クリティカルパスがほぼ半分になります。浮動小数点の非結合性のため、この最適化はコンパイラーにとって違法であることに注意してください。


これは、4ウェイノード分割とAMDFMA4命令を使用した代替バージョンです。融合乗算加算を使用できない場合は、自由に分割してください。上記の最初のバージョンよりも優れている可能性があります。

__m128 res0 = _mm_mul_ps(tj[ 0],qi[ 0]);
__m128 res1 = _mm_mul_ps(tj[ 1],qi[ 1]);
__m128 res2 = _mm_mul_ps(tj[ 2],qi[ 2]);
__m128 res3 = _mm_mul_ps(tj[ 3],qi[ 3]);

res0 = _mm_macc_ps(tj[ 4],qi[ 4],res0);
res1 = _mm_macc_ps(tj[ 5],qi[ 5],res1);
res2 = _mm_macc_ps(tj[ 6],qi[ 6],res2);
res3 = _mm_macc_ps(tj[ 7],qi[ 7],res3);

res0 = _mm_macc_ps(tj[ 8],qi[ 8],res0);
res1 = _mm_macc_ps(tj[ 9],qi[ 9],res1);
res2 = _mm_macc_ps(tj[10],qi[10],res2);
res3 = _mm_macc_ps(tj[11],qi[11],res3);

res0 = _mm_macc_ps(tj[12],qi[12],res0);
res1 = _mm_macc_ps(tj[13],qi[13],res1);
res2 = _mm_macc_ps(tj[14],qi[14],res2);
res3 = _mm_macc_ps(tj[15],qi[15],res3);

res0 = _mm_macc_ps(tj[16],qi[16],res0);
res1 = _mm_macc_ps(tj[17],qi[17],res1);
res2 = _mm_macc_ps(tj[18],qi[18],res2);
res3 = _mm_macc_ps(tj[19],qi[19],res3);

res0 = _mm_add_ps(res0,res1);
res2 = _mm_add_ps(res2,res3);

return _mm_add_ps(res0,res2);
于 2012-07-04T02:36:32.333 に答える
3

まず、実行できる最も重要な最適化は、コンパイラーですべての最適化設定がオンになっていることを確認することです。


コンパイラはかなり賢いので、ループとして書くと、次のように展開される可能性があります。

__128 res = _mm_setzero();
for (int i = 0; i < 10; i++) {
  res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])));
}
return res;

(GCCでは、合格する必要があり-funroll-loopsます。その後、GCCを展開して、一度に5回の反復を実行します。)

ループバージョンが遅い場合は、マクロを定義して手動で展開することもできます。例:

__128 res = _mm_setzero();

#define STEP(i) res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])))

STEP(0); STEP(1); STEP(2); STEP(3); STEP(4);
STEP(5); STEP(6); STEP(7); STEP(8); STEP(9);

#undef STEP

return res;

0から20までループを実行することもできます(またはマクロバージョンでも同じことを実行できます)。

__128 res = _mm_setzero();
for (int i = 0; i < 20; i++) {
  res = _mm_add_ps(res, _mm_mul_ps(tj[i], qi[i]));
}
return res;

(GCCを使用すると、-funroll-loopsこれは一度に10回の反復を実行するために展開されます。つまり、上記の一度に2つのループと同じです。)

于 2012-07-04T01:48:25.153 に答える
2

データは、特殊なSSE4ドット積命令()に適した形式でメモリに配置されていませんdpps。これらの命令は、次のように、単一のベクトルの次元が隣接していることを想定しています。

| dim0 | dim1 | dim2 | ... | dim19 |

一方、データにはベクトルが相互にインターリーブされているように見えます。

| v0-dim0 | v1-dim0 | v2-dim0 | v3-dim0 | v0-dim1 | ...

現在の一般的なアプローチは適切だと思われます。乗算の結果が生成された直後に使用されないように命令を並べ替えることで改善できる可能性がありますが、実際にはコンパイラはそれを独自に把握できるはずです。

于 2012-07-04T01:52:41.203 に答える