SIMD:なぜSSE RGBからYuvの色変換はC ++実装とほぼ同じ速度ですか?
-
26-10-2019 - |
質問
RGBからYUV420コンバーターを最適化しようとしました。ルックアップテーブルを使用すると、固定点算術を使用しても速度が上がりました。しかし、SSEの指示を使用して実際の利益を期待していました。私が最初に進むとコードが遅くなり、すべての操作をチェーンした後、元のコードとほぼ同じ速度になりました。私の実装に何か問題がありますか、それともSSEの指示は手元のタスクに適していませんか?
元のコードのセクションは次のとおりです。
#define RRGB24YUVCI2_00 0.299
#define RRGB24YUVCI2_01 0.587
#define RRGB24YUVCI2_02 0.114
#define RRGB24YUVCI2_10 -0.147
#define RRGB24YUVCI2_11 -0.289
#define RRGB24YUVCI2_12 0.436
#define RRGB24YUVCI2_20 0.615
#define RRGB24YUVCI2_21 -0.515
#define RRGB24YUVCI2_22 -0.100
void RealRGB24toYUV420Converter::Convert(void* pRgb, void* pY, void* pU, void* pV)
{
yuvType* py = (yuvType *)pY;
yuvType* pu = (yuvType *)pU;
yuvType* pv = (yuvType *)pV;
unsigned char* src = (unsigned char *)pRgb;
/// Y have range 0..255, U & V have range -128..127.
double u,v;
double r,g,b;
/// Step in 2x2 pel blocks. (4 pels per block).
int xBlks = _width >> 1;
int yBlks = _height >> 1;
for(int yb = 0; yb < yBlks; yb++)
for(int xb = 0; xb < xBlks; xb++)
{
int chrOff = yb*xBlks + xb;
int lumOff = (yb*_width + xb) << 1;
unsigned char* t = src + lumOff*3;
/// Top left pel.
b = (double)(*t++);
g = (double)(*t++);
r = (double)(*t++);
py[lumOff] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((int)(0.5 + RRGB24YUVCI2_00*r + RRGB24YUVCI2_01*g + RRGB24YUVCI2_02*b));
u = RRGB24YUVCI2_10*r + RRGB24YUVCI2_11*g + RRGB24YUVCI2_12*b;
v = RRGB24YUVCI2_20*r + RRGB24YUVCI2_21*g + RRGB24YUVCI2_22*b;
/// Top right pel.
b = (double)(*t++);
g = (double)(*t++);
r = (double)(*t++);
py[lumOff+1] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((int)(0.5 + RRGB24YUVCI2_00*r + RRGB24YUVCI2_01*g + RRGB24YUVCI2_02*b));
u += RRGB24YUVCI2_10*r + RRGB24YUVCI2_11*g + RRGB24YUVCI2_12*b;
v += RRGB24YUVCI2_20*r + RRGB24YUVCI2_21*g + RRGB24YUVCI2_22*b;
lumOff += _width;
t = t + _width*3 - 6;
/// Bottom left pel.
b = (double)(*t++);
g = (double)(*t++);
r = (double)(*t++);
py[lumOff] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((int)(0.5 + RRGB24YUVCI2_00*r + RRGB24YUVCI2_01*g + RRGB24YUVCI2_02*b));
u += RRGB24YUVCI2_10*r + RRGB24YUVCI2_11*g + RRGB24YUVCI2_12*b;
v += RRGB24YUVCI2_20*r + RRGB24YUVCI2_21*g + RRGB24YUVCI2_22*b;
/// Bottom right pel.
b = (double)(*t++);
g = (double)(*t++);
r = (double)(*t++);
py[lumOff+1] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((int)(0.5 + RRGB24YUVCI2_00*r + RRGB24YUVCI2_01*g + RRGB24YUVCI2_02*b));
u += RRGB24YUVCI2_10*r + RRGB24YUVCI2_11*g + RRGB24YUVCI2_12*b;
v += RRGB24YUVCI2_20*r + RRGB24YUVCI2_21*g + RRGB24YUVCI2_22*b;
/// Average the 4 chr values.
int iu = (int)u;
int iv = (int)v;
if(iu < 0) ///< Rounding.
iu -= 2;
else
iu += 2;
if(iv < 0) ///< Rounding.
iv -= 2;
else
iv += 2;
pu[chrOff] = (yuvType)( _chrOff + RRGB24YUVCI2_RANGECHECK_N128TO127(iu/4) );
pv[chrOff] = (yuvType)( _chrOff + RRGB24YUVCI2_RANGECHECK_N128TO127(iv/4) );
}//end for xb & yb...
}//end Convert.
そして、ここにSSEを使用するバージョンがあります
const float fRRGB24YUVCI2_00 = 0.299;
const float fRRGB24YUVCI2_01 = 0.587;
const float fRRGB24YUVCI2_02 = 0.114;
const float fRRGB24YUVCI2_10 = -0.147;
const float fRRGB24YUVCI2_11 = -0.289;
const float fRRGB24YUVCI2_12 = 0.436;
const float fRRGB24YUVCI2_20 = 0.615;
const float fRRGB24YUVCI2_21 = -0.515;
const float fRRGB24YUVCI2_22 = -0.100;
void RealRGB24toYUV420Converter::Convert(void* pRgb, void* pY, void* pU, void* pV)
{
__m128 xmm_y = _mm_loadu_ps(fCOEFF_0);
__m128 xmm_u = _mm_loadu_ps(fCOEFF_1);
__m128 xmm_v = _mm_loadu_ps(fCOEFF_2);
yuvType* py = (yuvType *)pY;
yuvType* pu = (yuvType *)pU;
yuvType* pv = (yuvType *)pV;
unsigned char* src = (unsigned char *)pRgb;
/// Y have range 0..255, U & V have range -128..127.
float bgr1[4];
bgr1[3] = 0.0;
float bgr2[4];
bgr2[3] = 0.0;
float bgr3[4];
bgr3[3] = 0.0;
float bgr4[4];
bgr4[3] = 0.0;
/// Step in 2x2 pel blocks. (4 pels per block).
int xBlks = _width >> 1;
int yBlks = _height >> 1;
for(int yb = 0; yb < yBlks; yb++)
for(int xb = 0; xb < xBlks; xb++)
{
int chrOff = yb*xBlks + xb;
int lumOff = (yb*_width + xb) << 1;
unsigned char* t = src + lumOff*3;
bgr1[2] = (float)*t++;
bgr1[1] = (float)*t++;
bgr1[0] = (float)*t++;
bgr2[2] = (float)*t++;
bgr2[1] = (float)*t++;
bgr2[0] = (float)*t++;
t = t + _width*3 - 6;
bgr3[2] = (float)*t++;
bgr3[1] = (float)*t++;
bgr3[0] = (float)*t++;
bgr4[2] = (float)*t++;
bgr4[1] = (float)*t++;
bgr4[0] = (float)*t++;
__m128 xmm1 = _mm_loadu_ps(bgr1);
__m128 xmm2 = _mm_loadu_ps(bgr2);
__m128 xmm3 = _mm_loadu_ps(bgr3);
__m128 xmm4 = _mm_loadu_ps(bgr4);
// Y
__m128 xmm_res_y = _mm_mul_ps(xmm1, xmm_y);
py[lumOff] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((xmm_res_y.m128_f32[0] + xmm_res_y.m128_f32[1] + xmm_res_y.m128_f32[2] ));
// Y
xmm_res_y = _mm_mul_ps(xmm2, xmm_y);
py[lumOff + 1] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((xmm_res_y.m128_f32[0] + xmm_res_y.m128_f32[1] + xmm_res_y.m128_f32[2] ));
lumOff += _width;
// Y
xmm_res_y = _mm_mul_ps(xmm3, xmm_y);
py[lumOff] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((xmm_res_y.m128_f32[0] + xmm_res_y.m128_f32[1] + xmm_res_y.m128_f32[2] ));
// Y
xmm_res_y = _mm_mul_ps(xmm4, xmm_y);
py[lumOff+1] = (yuvType)RRGB24YUVCI2_RANGECHECK_0TO255((xmm_res_y.m128_f32[0] + xmm_res_y.m128_f32[1] + xmm_res_y.m128_f32[2] ));
// U
__m128 xmm_res = _mm_add_ps(
_mm_add_ps(_mm_mul_ps(xmm1, xmm_u), _mm_mul_ps(xmm2, xmm_u)),
_mm_add_ps(_mm_mul_ps(xmm3, xmm_u), _mm_mul_ps(xmm4, xmm_u))
);
float fU = xmm_res.m128_f32[0] + xmm_res.m128_f32[1] + xmm_res.m128_f32[2];
// V
xmm_res = _mm_add_ps(
_mm_add_ps(_mm_mul_ps(xmm1, xmm_v), _mm_mul_ps(xmm2, xmm_v)),
_mm_add_ps(_mm_mul_ps(xmm3, xmm_v), _mm_mul_ps(xmm4, xmm_v))
);
float fV = xmm_res.m128_f32[0] + xmm_res.m128_f32[1] + xmm_res.m128_f32[2];
/// Average the 4 chr values.
int iu = (int)fU;
int iv = (int)fV;
if(iu < 0) ///< Rounding.
iu -= 2;
else
iu += 2;
if(iv < 0) ///< Rounding.
iv -= 2;
else
iv += 2;
pu[chrOff] = (yuvType)( _chrOff + RRGB24YUVCI2_RANGECHECK_N128TO127(iu >> 2) );
pv[chrOff] = (yuvType)( _chrOff + RRGB24YUVCI2_RANGECHECK_N128TO127(iv >> 2) );
}//end for xb & yb...
}
これはSSE2での私の最初の試みの1つなので、おそらく何かが足りないのでしょうか?参考までに、Visual Studio 2008を使用してWindowsプラットフォームで作業しています。
解決
いくつかの問題:
誤った負荷を使用しています - これらは非常に高価です(Nehalem、別名Core i5/Core i7を除く) - 少なくとも2倍の整列負荷のコスト - 負荷後に十分な計算がある場合はコストを償却できますこのケースは比較的少ないです。これらの16バイトをアライメントし、整列した負荷を使用することにより、BGR1、BGR2などの負荷に対してこれを修正できます。 [さらに良いことに、これらの中間アレイをまったく使用しないでください - メモリからSSEレジスタに直接データをロードし、SIMDですべてのシャッフルなどを行います - 以下を参照
スカラーとSIMDコードの間を行き来する - スカラーコードはおそらくパフォーマンスに関する限り、支配的な部分になるので、SIMDの利益はこれによって圧倒される傾向があります - あなたは本当にする必要があります すべての SIMDの指示を使用してループ内の内部(つまり、スカラーコードを取り除く)
他のヒント
インシントリックの代わりにインラインアセンブリ命令を使用できます。コードの速度が少し増加する可能性があります。しかし、インラインアセンブリはコンパイラ固有です。とにかく、Paul Rの回答で述べたように、フルスピードを達成するには、Alignedデータを使用する必要があります。しかし、データの調整はさらにコンパイラ固有のものです:)
コンパイラを変更できる場合は、Windows用のIntelコンパイラを試すことができます。特にインラインアセンブリコードの方がはるかに優れているとは思わないが、見ても間違いないだろう。
あなたのアプローチにいくつかの問題があります:
C ++バージョンはポインターTから「ダブルR、G、B」にロードされ、おそらく、コンパイラはこれらをFPレジスタへの負荷に直接最適化しました。つまり、「Double R、G、B」時間。ただし、バージョンでは、「Float BGR0/1/2/3」にロードしてから、_MM_LoadU_PSを呼び出します。 「Float Bgr0/1/2/3」がメモリになっている場合、私は驚かないでしょう。
代わりにイントシクスインラインアセンブリを使用しています。これらの__m128変数のすべてではないにしても、一部のものはまだメモリにある可能性があります。繰り返しますが、余分な読み取りと記憶に書き込みます。
ほとんどの作業はおそらくRRGB24YUVCI2 _*()で行われており、これらを最適化しようとはしていません。
変数のいずれかを調整しているわけではありませんが、それは余分なメモリアクセスに対する追加のペナルティにすぎません。これらを最初に排除してみてください。
最善の策は、既存の最適化されたRGB/YUV変換ライブラリを見つけて使用することです。