2011-01-28 20 views
6

Ho appena provato ad ottimizzare un convertitore da RGB a YUV420. L'utilizzo di una tabella di ricerca ha prodotto un aumento di velocità, come l'utilizzo dell'aritmetica a virgola fissa. Tuttavia mi aspettavo dei veri guadagni usando le istruzioni SSE. Il mio primo tentativo è stato quello di ottenere un codice più lento e, dopo aver incatenato tutte le operazioni, ha all'incirca la stessa velocità del codice originale. C'è qualcosa di sbagliato nella mia implementazione o le istruzioni SSE non sono adatte al compito in questione?SIMD: Perché la conversione del colore da SSE RGB a YUV ha la stessa velocità dell'implementazione C++?

Una sezione del codice originale segue:

#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. 

E qui è la versione che utilizza 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... 
} 

Questo è uno dei miei primi tentativi di SSE2 quindi forse mi manca qualcosa? FYI Sto lavorando sulla piattaforma Windows utilizzando Visual Studio 2008.

risposta

8

un paio di problemi:

  • si sta utilizzando carichi non allineate - questi sono piuttosto costosi (a parte su Nehalem aka Core i5/core i7) - almeno 2 volte il costo di un carico allineato - il costo può essere ammortizzato se si ha un sacco di calcolo dopo i carichi, ma in questo caso si ha relativamente poco. È possibile correggere questo per i carichi da bgr1, bgr2, ecc, rendendo questi 16 byte allineati e utilizzando carichi allineati. [Meglio ancora, non usare affatto questi array intermedi - carica i dati direttamente dalla memoria ai registri SSE e fai tutto il tuo shuffling ecc con SIMD - vedi sotto]

  • stai andando avanti e indietro tra scalare e SIMD codice - il codice scalare sarà probabilmente la parte dominante per quanto riguarda le prestazioni, quindi qualsiasi guadagno di SIMD tenderà a essere sommerso da questo - devi davvero fare tutto il all'interno del tuo loop usando le istruzioni SIMD (es. codice scalare)

+0

Ciao Paul, grazie per la tua risposta. Ho modificato tutti gli array ora allineati a 16 byte e sto usando _mm_load_ps invece di _mm_loadu_ps. Finora non riesco a vedere alcuna differenza notevole però. Per quanto riguarda il tuo secondo suggerimento, scusami la mia ignoranza: come posso evitare di passare dallo scalare al codice SIMD? Non capisco come posso liberarmi del codice scalare. – Ralf

+0

@Ralf: questa è la parte difficile, cioè pensare ai modi SIMD per ciò che si potrebbe fare altrimenti con il codice scalare. Idealmente, è necessario caricare i dati direttamente nei registri SSE dalla memoria, quindi riorganizzare gli elementi nella disposizione richiesta, eseguire i calcoli, riorganizzarli nella disposizione di output richiesta, quindi archiviarli direttamente nella memoria dai registri SSE. . Se hai SSSE3 (aka SSE3.5) o meglio lo shuffling degli elementi se molto più semplice (PSHUFB) - con SSE3 e versioni precedenti è ancora possibile, ma un po 'più complicato, dal momento che sono disponibili istruzioni shuffle limitate. –

+0

Ok, grazie Paul, lasciami fare qualche altra ricerca su SIMD :) – Ralf

1

È possibile utilizzare istruzioni di montaggio inline anziché insintrics. Potrebbe aumentare leggermente la velocità del tuo codice. Ma l'assemblaggio in linea è specifico del compilatore. In ogni caso, come affermato nella risposta di Paul R, è necessario utilizzare i dati allineati per raggiungere la massima velocità. Ma l'allineamento dei dati è ancora più specifico del compilatore :)

Se è possibile modificare il compilatore, è possibile provare il compilatore Intel per Windows. Dubito che sarebbe molto meglio, soprattutto per il codice assembly inline, ma sicuramente vale la pena guardare.

+0

Ciao John, ho provato ad allineare i dati, purtroppo senza risultati. Grazie per la tua risposta. – Ralf

+0

Hm .... Hai provato solo l'allineamento? Poiché si tenta di caricare i dati nel registro xmm di _mm_loadu_ps (float *) (si associa all'istruzione MOVUPS), si dice al processore di caricare i dati non allineati. Non è sufficiente allineare i dati, devi usare le istruzioni appropriate. Per il tuo caso è _mm_load_ps (float *) (si associa all'istruzione MOVAPS). Se questa funzione fallisce, significa che qualcosa non va nel tuo allineamento. – JohnGray

+0

Grazie per la tua risposta John, l'ho visto solo ora ... Sì, ho cambiato tutte le istruzioni per usare _mm_load_ps ma non sembrava fare alcuna differenza. – Ralf

0

Vedo qualche problema con il vostro approccio:

  1. La versione carichi da puntatore t C++ per "Double R, G, B", e con ogni probabilità, il compilatore ha ottimizzato questi in carico a FP registra direttamente, cioè "double r, g, b" vive nei registri in fase di esecuzione. Ma nella tua versione, carichi in "float bgr0/1/2/3" e poi chiama _mm_loadu_ps. Non sarò sorpreso se "float bgr0/1/2/3" è in memoria, questo significa che hai letture e scritture extra in memoria.

  2. Si sta utilizzando l'assemblaggio intrinseco invece inline. Alcune, se non tutte, di quelle __m128 variabili potrebbero essere ancora in memoria. Ancora una volta, letture e scritture extra in memoria.

  3. La maggior parte del lavoro viene probabilmente eseguita in RRGB24YUVCI2 _ *() e non si sta tentando di ottimizzarli.

Non stai allineando nessuna delle tue variabili, ma questa è solo una penalità aggiuntiva per l'accesso extra alla memoria, cerca di eliminarle prima.

La soluzione migliore è trovare una libreria di conversione RGB/YUV esistente e ottimizzata e utilizzarla.

+0

Grazie per il vostro feedback. Una domanda: come ottimizzerei RRGB24YUVC12_ *? Vuoi dire che dovrei ottimizzare il controllo del range in qualche modo? La ricerca di una libreria ottimizzata esistente ha vanificato lo scopo: la conversione del colore era solo un test per vedere come SIMD poteva essere applicato agli algoritmi di elaborazione video. – Ralf

Problemi correlati