2011-01-28 31 views
6

Acabo de tratar de optimizar un convertidor RGB a YUV420. El uso de una tabla de búsqueda arrojó un aumento de velocidad, al igual que el uso de aritmética de punto fijo. Sin embargo, estaba esperando las ganancias reales usando las instrucciones de SSE. Mi primer intento resultó en un código más lento y después de encadenar todas las operaciones, es aproximadamente la misma velocidad que el código original. ¿Hay algo mal en mi implementación o las instrucciones SSE simplemente no son adecuadas para la tarea en cuestión?SIMD: ¿Por qué la conversión de color SSE RGB a YUV es aproximadamente la misma velocidad que la implementación de C++?

Una sección del código original sigue:

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

Y aquí es la versión que utiliza 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... 
} 

Este es uno de mis primeros intentos de SSE2 así que quizás me falta algo? FYI estoy trabajando en la plataforma Windows con Visual Studio 2008.

Respuesta

8

Un par de problemas:

  • que estés utilizando cargas desalineadas - estos son bastante caros (aparte de en Nehalem también conocido como Core i5/Core i7) - al menos 2 veces el costo de una carga alineada - el costo puede amortizarse si tiene mucho cómputo después de las cargas, pero en este caso tiene relativamente poco. Puede arreglar esto para las cargas de bgr1, bgr2, etc. haciendo que estos 16 bytes estén alineados y utilizando cargas alineadas. [Mejor aún, no use estos arreglos intermedios - cargue datos directamente desde la memoria a los registros SSE y realice todos los cambios, con SIMD - vea debajo]

  • Usted va y viene entre escalar y SIMD código: el código escalar probablemente sea la parte dominante en lo que se refiere al rendimiento, por lo que cualquier ganancia SIMD tenderá a quedar saturada por esto; realmente necesita hacer todo dentro de su bucle usando instrucciones SIMD (es decir, deshacerse del código escalar)

+0

Hola, Paul, gracias por tu respuesta. He modificado todas las matrices para que estén alineadas a 16 bytes ahora y estoy usando _mm_load_ps en lugar de _mm_loadu_ps. Hasta ahora, no puedo ver ninguna diferencia notable. Con respecto a su segunda sugerencia, disculpe mi ignorancia: ¿cómo puedo evitar cambiar entre el código escalar y SIMD? No entiendo cómo puedo deshacerme del código escalar. – Ralf

+0

@Ralf: esa es la parte difícil, es decir, pensar en formas SIMD de lo que de otro modo podrías hacer con el código escalar. Idealmente, debería cargar sus datos directamente a los registros SSE de la memoria, luego reorganizar los elementos en la disposición requerida, hacer los cálculos, reorganizar de nuevo en la disposición de salida requerida, luego almacenar directamente en la memoria de los registros SSE. . Si tiene SSSE3 (también conocido como SSE3.5) o mejor, la reorganización de los elementos es mucho más fácil (PSHUFB): con SSE3 y versiones anteriores aún es posible, pero un poco más complicado, ya que hay instrucciones limitadas para mezclar disponibles. –

+0

Bien, gracias Paul, déjame investigar un poco más sobre SIMD :) – Ralf

1

Puede utilizar las instrucciones de montaje en línea en lugar de insintrics. Puede aumentar un poco la velocidad de tu código. Pero el ensamblaje en línea es específico del compilador. De todos modos, como se indica en la respuesta de Paul R, debe usar datos alineados para alcanzar la velocidad máxima. Pero la alineación de datos es aún más algo específico del compilador :)

Si puede cambiar el compilador, puede probar el compilador Intel para Windows. Dudo que sea mucho mejor, especialmente para el código ensamblador en línea, pero definitivamente vale la pena buscarlo.

+0

Hola, John, traté de alinear los datos, desafortunadamente en vano. Gracias por tu respuesta. – Ralf

+0

Hm .... ¿Intentó alinear solo? Como intenta cargar sus datos en xmm registrándose mediante _mm_loadu_ps (float *) (asigna a la instrucción MOVUPS), le dice al procesador que cargue datos desalineados. No es suficiente alinear solo los datos, debe usar las instrucciones adecuadas. Para su caso, es _mm_load_ps (float *) (se asigna a la instrucción MOVAPS). Si esta función falla, significa que algo está mal con su alineación. – JohnGray

+0

Gracias por su respuesta John, solo lo vi ahora ... Sí, cambié todas las instrucciones para usar _mm_load_ps pero no pareció hacer ninguna diferencia. – Ralf

0

I ver algunos problemas con su enfoque:

  1. El C++ cargas versión de puntero t en "doble r, g, b", y con toda probabilidad, el compilador ha optimizado estos en la carga a FP se registra directamente, es decir, "double r, g, b" vive en registros en tiempo de ejecución. Pero en su versión, carga en "float bgr0/1/2/3" y luego llama a _mm_loadu_ps. No me sorprendería si "float bgr0/1/2/3" está en la memoria, esto significa que tiene lecturas adicionales y escribe en la memoria.

  2. Está utilizando intrinsics en lugar de ensamblaje en línea. Algunas, si no todas, de esas __m128 variables aún pueden estar en la memoria. Nuevamente, extra lee y escribe en la memoria.

  3. La mayoría de los trabajos probablemente se realicen en RRGB24YUVCI2 _ *() y no está tratando de optimizarlos.

Usted no está alineando cualquiera de sus variables, pero eso es sólo penalización adicional por su acceso a la memoria adicional, intenta eliminar estos primeros.

Su mejor opción es encontrar una biblioteca de conversión RGB/YUV optimizada existente y utilizarla.

+0

Gracias por su comentario. Una pregunta: ¿cómo optimizaría RRGB24YUVC12_ *? ¿Quieres decir que debería optimizar el control de rango de alguna manera? Encontrar un tipo de biblioteca optimizada existente frustra el objetivo: la conversión de color fue solo una prueba para ver cómo se podría aplicar SIMD a los algoritmos de procesamiento de video. – Ralf

Cuestiones relacionadas