2011-01-28 17 views
6

Właśnie próbowałem zoptymalizować konwerter RGB na YUV420. Korzystanie z tabeli przeglądowej spowodowało wzrost prędkości, podobnie jak w przypadku arytmetyki stałoprzecinkowej. Spodziewałem się jednak prawdziwych korzyści za pomocą instrukcji SSE. Moje pierwsze odwiedziny spowodują spowolnienie kodu i po powiązaniu wszystkich operacji są mniej więcej takie same, jak w oryginalnym kodzie. Czy jest coś złego w mojej implementacji lub instrukcje SSE po prostu nie pasują do wykonywanego zadania?SIMD: Dlaczego konwersja kolorów SSE RGB na YUV ma tę samą szybkość, co implementacja C++?

Część oryginalnego kodu następująco:

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

A oto wersję przy użyciu 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... 
} 

Jest to jeden z moich pierwszych prób SSE2 więc może ja czegoś brakuje? FYI Ja pracuje na platformie Windows przy użyciu programu Visual Studio 2008.

Odpowiedz

8

Kilka problemów:

  • używasz niewłaściwie ładunki - są dość drogie (oprócz na Nehalem aka Core i5/rdzeń i7) - co najmniej 2x koszt wyrównanego obciążenia - koszt może zostać zamortyzowany, jeśli masz mnóstwo obliczeń po obciążeniu, ale w tym przypadku masz stosunkowo mało. Możesz to naprawić dla obciążeń z bgr1, bgr2, itp., Ustawiając te 16 bajtów wyrównane i używając wyrównanych obciążeń. [Lepiej jednak, nie używaj w ogóle tych pośrednich tablic - ładuj dane bezpośrednio z pamięci do rejestrów SSE i wykonuj wszystkie swoje tasowanie itd. Za pomocą SIMD - patrz poniżej]

  • Wracasz z skalarnego do SIMD kod - kod skalarny prawdopodobnie będzie dominującą częścią pod względem wydajności, więc wszelkie zyski SIMD będą miały tendencję do tego, by być zalewanym przez to - naprawdę musisz zrobić wszystko w swoim pętli za pomocą instrukcji SIMD (tj. pozbyć się Kod skalarne)

+0

Cześć Paul, dziękuję za odpowiedź. Zmodyfikowałem wszystkie tablice na 16 bajtów wyrównane teraz i używam _mm_load_ps zamiast _mm_loadu_ps. Jak dotąd nie widzę żadnej zauważalnej różnicy. Co do twojej drugiej sugestii, proszę wybaczyć moją niewiedzę: jak mogę uniknąć przełączania się między skalarem a kodem SIMD? Nie rozumiem, jak mogę się pozbyć skalarnego kodu. – Ralf

+0

@Ralf: To jest trudna część, to znaczy myślenie o SIMD, co możesz zrobić z kodem skalarnym. Najlepiej byłoby załadować dane bezpośrednio do rejestrów SSE z pamięci, a następnie ponownie zorganizować elementy do wymaganego układu, wykonać obliczenia, ponownie zorganizować z powrotem do wymaganego układu wyjściowego, a następnie zapisać bezpośrednio w pamięci z rejestru (rejestrów) SSE . Jeśli masz SSSE3 (aka SSE3.5) lub lepszy, tasowanie elementów, o ile jest to łatwiejsze (PSHUFB) - z SSE3 i wcześniejszymi wersjami jest nadal możliwe, ale trochę trudniejsze, ponieważ dostępne są ograniczone instrukcje shuffle. –

+0

Ok, dzięki Paul, pozwól mi zrobić więcej badań na temat SIMD :) – Ralf

1

można użyć wbudowanych instrukcji montażu zamiast insintrics. Może trochę zwiększyć szybkość twojego kodu. Ale wbudowany zestaw jest specyficzny dla kompilatora. W każdym razie, jak stwierdzono w odpowiedzi Paula R, musisz użyć wyrównanych danych, aby osiągnąć pełną prędkość. Ale wyrównanie danych jest jeszcze bardziej specyficzne dla kompilatora :)

Jeśli możesz zmienić kompilator, możesz wypróbować kompilator Intel dla Windows. Wątpię, by było o wiele lepiej, zwłaszcza w przypadku wbudowanego kodu montażowego, ale zdecydowanie warto go obejrzeć.

+0

Cześć John, próbowałem wyrównać dane, niestety bez skutku. Dzięki za odpowiedź. – Ralf

+0

Hm .... Czy próbowałeś tylko wyrównywać? Ponieważ próbujesz załadować swoje dane do rejestru xmm przez _mm_loadu_ps (float *) (mapuje do instrukcji MOVUPS), mówisz procesorowi, aby załadował niepasujące dane. Nie wystarczy tylko wyrównywać dane, musisz użyć odpowiedniej instrukcji. Dla twojego przypadku jest to _mm_load_ps (float *) (mapuje do instrukcji MOVAPS). Jeśli ta funkcja się nie powiedzie, oznacza to, że coś jest nie tak z twoim wyrównaniem. – JohnGray

+0

Dzięki za odpowiedź John, zobaczyłem ją tylko teraz ... Tak, zmieniłem wszystkie instrukcje, aby używać _mm_load_ps, ale nie wydawało się to żadnego znaczenia. – Ralf

0

Widzę kilka problemów z podejściem:

  1. C++ wersja ładunku wskaźnik t do „Double R, G, B”, a według wszelkiego prawdopodobieństwa, kompilator zoptymalizowane są do załadunku do FP rejestruje bezpośrednio, tj. "Double r, g, b" żyje w rejestrach w czasie wykonywania. Ale w twojej wersji ładujesz do "float bgr0/1/2/3", a następnie wywołuje _mm_loadu_ps. Nie będę zaskoczony, jeśli "float bgr0/1/2/3" są w pamięci, oznacza to, że masz dodatkowe odczyty i zapisy do pamięci.

  2. Używasz wbudowanego zestawu elementów wewnętrznych. Niektóre, jeśli nie wszystkie, zmiennych __m128 mogą nadal znajdować się w pamięci. Ponownie, dodatkowe odczyty i zapisuje do pamięci.

  3. Większość prac prawdopodobnie wykonuje się w RRGB24YUVCI2 _ *() i nie próbujesz ich optymalizować.

Nie dostosowujesz żadnej ze swoich zmiennych, ale to tylko dodatkowa kara za dodatkowy dostęp do pamięci, spróbuj ją najpierw wyeliminować.

Najlepiej jest znaleźć istniejącą, zoptymalizowaną bibliotekę konwersji RGB/YUV i używać jej.

+0

Dziękujemy za opinię. Jedno pytanie: jak zoptymalizować RRGB24YUVC12_ *? Masz na myśli, że powinienem jakoś zoptymalizować kontrolę zasięgu? Znalezienie istniejącego zoptymalizowanego rodzaju biblioteki jest sprzeczne z celem: konwersja kolorów była tylko testem, aby sprawdzić, w jaki sposób SIMD można zastosować do algorytmów przetwarzania wideo. – Ralf