YUV420planar zu BGR Konvertierung ist kritisch langsam

Für Fragen zu Grafik APIs wie DirectX und OpenGL sowie Shaderprogrammierung.
Antworten
Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 20.06.2019, 10:24

Hallo Freunde der IT!

Heute brauche ich Hilfe/Rat von denen von Euch, die sich mit Code-Optimierung auskennen.

Ich habe in den letzten Wochen einen rudimentären Video-Player geschrieben (libvpx, libwebm, libopus). Die Videocodecs spucken am Ende YUV420planar Bilder aus. Soweit, so gut.

Um am Ende in meinem RGB/BGR basierten „Universum“ klarzukommen, mache ich eine Transformation von YU420planar zu BGR (8 bit/channel) auf der CPU.

Mein Testvideo ist ein Clip mit 720p.

Je nach CPU ist es unmöglich, eine flüssige Video-Wiedergabe zu bekommen – nicht, weil der lahme VP8/VP9 so limitieren, sondern weil ich die YUV-BGR-Konvertierung nicht schnell genug hinbekomme.

Auf meinen schnelleren Test-Rechnern (i7 Laptop – Strom angestöpselt; Ryzen PC) braucht die Konvertierung bei besagten Testvideo ca. 8-11ms. Das reicht fürs Playback dicke.

Auf „schwächeren“ Rechnern, wenn zum Beispiel der Laptop runtertaktet (mobilmodus, Strom ausgestöpselt) oder auf Celeron-basierten Mini-PCs geht die Zeit für die Konvertierung hoch auf >45ms. Das reicht dann nicht mehr für flüssiges Playback bei 25 fps.

Ich frage mich, ob ich einen groben Fehler in der Konvertierung mache, ob ich die Sache falsch angehe.
Klar könnte ich die Konvertierung auf der GPU machen. Aber ich frage mich, ob dieses Thema WIRKLICH son CPU-Killer ist oder meine Implementierung einfach Mist ist. Hier ist besagte Methode - inhaltlich funktioniert Sie super:

Code: Alles auswählen


void convertPlanesYUV420pToBGR(const unsigned char* PlaneY, const unsigned char* PlaneU, const unsigned char* PlaneV, unsigned char* TargetBuffer, int ResX, int ResY)
{
    const int _HalfResX(ResX/2);

    for (int CntY=0; CntY<ResY; CntY++) {

        const int _HalfLineYMulHalfResX( (CntY/2) * _HalfResX);
        const int _ResXMulLineY(CntY * ResX);

        for (int CntX=0; CntX<ResX; CntX++) {
            const long int _ColorPlaneIndex (_HalfLineYMulHalfResX + (CntX/2) );

            const short int _ValY (PlaneY[_ResXMulLineY + CntX] - 16);
            const short int _ValU (PlaneU[_ColorPlaneIndex] - 128);
            const short int _ValV (PlaneV[_ColorPlaneIndex] - 128);

            /// convert the blue component
            const float _Blue(_ValY + (1.140f*_ValU)  );
            if (_Blue > 255.0f) {
                (*TargetBuffer) = 255;
            }
            else {
                if (_Blue < 0.0f) {
                     (*TargetBuffer) = 0;
                }
                else {
                    (*TargetBuffer) = static_cast<unsigned char>(_Blue);
                }
            }
            TargetBuffer++;


            /// convert the green component
            const float _Green(_ValY - (0.395f*_ValU) - (0.581*_ValV) );
            if (_Green > 255.0f) {
                (*TargetBuffer) = 255;
            }
            else {
                if (_Green < 0.0f) {
                    (*TargetBuffer) = 0;
                }
                else {
                    (*TargetBuffer) = static_cast<unsigned char>(_Green);
                }
            }
            TargetBuffer++;

            /// convert the red component
            const float _Red( _ValY + (2.032*_ValV) );
            if (_Red > 255.0f) {
                (*TargetBuffer) = 255;
            }
            else {
                if (_Red < 0.0f) {
                     (*TargetBuffer) = 0;
                }
                else {
                   (*TargetBuffer) = static_cast<unsigned char>(_Red);
                }
            }
            TargetBuffer++;


        }
    }
}
Die Ausführzeit skaliert auch schön mit der Auflösung des Videos (Pixelanzahl).
Der Profiler sagt, dass sich die CPU während des Playbacks die größte Zeit in dieser Methode aufhält.

Besonders kritisch sind die drei Blöcke, wo die Farbskalierung, das Clipping und das Schreiben in den Zielpuffer gemacht wird. Ich habe schon mit verschiedenen Datentypen/Casts für verschiedene Stellen dort rumgespielt – ohne Erfolg.

Habt Ihr nen Vorschlag, was ich an der Implementierung ändern könnte?

Wie machen andere das – gerade schwache Plattformen wie ARM-Systeme/Handys? Müssen die die GPU benutzen? Ist sone „popelige“ Farbraumkonvertierung wirklich sone Geschwindigkeitsherausforderung?

Besten Dank für Hinweie schonmal!
--
Verallgemeinerungen sind IMMER falsch.

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 20.06.2019, 10:38

Das ist 100 % prädestiniert für SIMD-Optimierung, und so machen es auch alle „schwachen“ Plattformen.

Aber fangen wir langsam an – aus dem Kopf müsste das hier:

Code: Alles auswählen

const float _Blue(_ValY + (1.140f*_ValU)  );
            if (_Blue > 255.0f) {
                (*TargetBuffer) = 255;
            }
            else {
                if (_Blue < 0.0f) {
                     (*TargetBuffer) = 0;
                }
                else {
                    (*TargetBuffer) = static_cast<unsigned char>(_Blue);
                }
            }
            TargetBuffer++;
besser durch sowas ersetzt werden:

Code: Alles auswählen

__m128 _Blue = _mm_set_ss(_ValY + (1.140f*_ValU)  );
_Blue = _mm_min_ss(_Blue, _mm_set_ss(255.0f));
_Blue = _mm_max_ss(_Blue, _mm_set_ss(0.0f));
*TargetBuffer++ = static_cast<unsigned char>(_mm_cvt_ss2si(_Blue));
Jetzt hast du schonmal deutlich weniger Sprünge.

Weiteres Problem: Ich zähle sieben short-zu-float-Konvertierungen bei den Zugriffen auf _ValY, _ValU, _ValV. Konvertier die direkt zu float oder besser zu __m128 durch _mm_cvtepi32_ps(_mm_cvtsi32_si128(dein_short)). Dann ersetzt du + - * durch _mm_add_ss() _mm_sub_ss() _mm_mul_ss().

Danach: Der Code, den ich dir vorgeschlagen habe, ist skalar und hat falsche Abhängigkeiten auf vorherige Registerwerte. Wenn du ihn tatsächlich datenparallel gestaltest – also R, G, B im selben __m128 packst und die *_ps-Versionen verwendest – hast du nur noch die Hälfte der Rechenoperationen und die falschen Abhängigkeiten entfallen, mit entsprechender Leistungssteigerung.

Schreiben pro Byte ist außerdem problematisch weil in der CPU nicht die Anzahl geschriebener Bytes der Flaschenhals ist, sondern die Anzahl der Schreiboperationen. Wenn du garantieren kannst, dass deine Videobreite ein Vielfaches von 4 ist, solltest du *unbedingt* immer vier Pixel parallel verarbeiten (also zwölf Werte) und sie als 8 + 4 B schreiben (Code dafür habe ich bei Bedarf). Auf x64 gern acht Pixel (24 Werte) als 3×8 B dank höherer Registerzahl. Wenn du dabei in den SSE-Registern bleibst, sollte das mindestens 20× so schnell sein wie jetzt (bis Memory Bandwidth Saturation einsetzt).
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 20.06.2019, 11:02

Moin Krishty!

Wow – danke für die schnelle und (Achtung: Unterstellung) – auch gute Antwort.
Ich habe tatsächlich „geahnt“, dass SIMD-Zeug ein Ansatz sein könnte – habe bisher nur noch nie etwas damit gemacht und das hat mich stark davon abgehalten, hier tiefer nachzuforschen.

Außerdem haben mich Code-Samples zu diesem Thema immer „erschlagen“, sobald ich in die Nähe des Themas gekommen bin. Deine Zeilen sind minimalistisch und ich gucke mal, was ich damit tun kann.

Außerdem war ich nicht sicher, welches Potential ich damit (SIMD) haben könnte (dachte an sowas wie Faktor 2 oder 3 – was ja auch schon gut wäre). Das Faktor 10+ mit gutem Code auf der CPU denkbar wäre, finde ich fazinierend. Die Art und Weise, hier „parallel“ zu denken, fällt mir noch schwer.

Ich werde das, was du geschrieben hast, zu gegebener Zeit mal versuchen, einzuatmen und versuchen, damit etwas sinnvolles umzusetzen. Hoffentlich heute Abend.

Ich werde Feedback geben, wenn ich es geschafft habe oder sich neue Fragen auftun.

Danke für den Impuls und die Hinweise,
Jens
--
Verallgemeinerungen sind IMMER falsch.

Benutzeravatar
Schrompf
Moderator
Beiträge: 3905
Registriert: 26.02.2009, 00:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Schrompf » 20.06.2019, 11:19

Ich bin neugierig, wieviel das bringt. Ich würde zuerst auf die Vergleiche tippen - wenn der Compiler dort nicht eh schon ConditionalMoves ausspuckt, was Du am ASM überprüfen müsstest. Dann würde ich drei Byte auf einmal lesen, in drei einzelne Floats konvertieren, und die dann jeweils in Dreiergruppen multiplizieren, minmaxen, konvertieren. Die Chancen stehen gut, dass der Compiler dann eh schon zu vektorisieren anfängt. Drei Byte sind zwar zwangweise immer unaligned, aber x86 steckt das nach meinem Wissen ohne größere Einbußen weg.

Die manuelle SIMD-Konstruktion ist aber in jedem Fall schneller. Du könntest noch ausprobieren, ob die Vermeidung des float-Umwegs was bringt. Also drei int16_t auf je einen int32_t verteilen, drei int32_t mit der jeweiligen Konstante * 16384 multiplizieren, Integer-MinMax, Bit-Shift >> 14, jetzt dürften alle drei ints im Bereich [0..255] sein und Du kannst die Bits wieder zu nem 3*8Bits-uint32_t zusammenstecken. Festkomma-Arithmetik mit signed-Typen hat ne gewisse Gefahr, in C++-UndefinedBehaviour oder sowas zu stolpern, wenn man nicht aufpasst, aber neuere SSE-Befehlssätze erlauben auch die parallele Verarbeitung von 4x, 8x oder 16x int32_t. Der Compiler dürfte die sogar benutzen, wenn Du ihm die richtigen arch-Flags mitgibst.
Häuptling von Dreamworlds. Baut an was Neuem. Hilft nebenbei nur höchst selten an der Open Asset Import Library mit.

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 20.06.2019, 12:35

Schrompf hat geschrieben:
20.06.2019, 11:19
Du könntest noch ausprobieren, ob die Vermeidung des float-Umwegs was bringt.
Genau die Idee hatte ich gerade auch noch, aber alles danach ziemlich anders.

SSE2 hat ja extra für Bild- und Videoverarbeitung Saturated Integer Arithmetics. Damit entfällt das Clamping komplett, und für 16-Bit-Integer steht ja via _mm_madd_epi16() auch ein komplettes Skalarprodukt zur Verfügung.

Das Zurückpacken zu 8-Bit machst du nicht via Shift, sondern via _mm_packus_epi16(). Das wendet dabei ohne Zutun Unsigned Saturation, also Clamping zu [0, 255], an.

Wenn dein Ryzen AVX512 kann, kannst du theoretisch in jedem Takt via _mm512_madd_epi16() 32 16-Bit-Werte multiply-adden (also zehn komplette Pixel) und bekommst danach via _mm512_packus_epi16 das Clamping zu [0, 255] ebenfalls in einem Takt.

Ab dem Zeitpunkt wäre wortwörtlich alles andere im Programm der Flaschenhals.

FYI: ARM Neon erlaubt Shifts in fast jeder Anweisung; um da den idealen Instruction Flow zu erreichen bringt es also absolut gar nichts, sich irgendwelche SIMD-fähigen Vektorklassen zu schreiben. Der einzig gangbare Weg ist, logisch geschlossene Funktionen wie deine YUV-Konvertierung als komplettes Stück für jede Plattform zu SIMD-isieren.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 20.06.2019, 12:59

Schrompf hat geschrieben:
20.06.2019, 11:19
wenn der Compiler dort nicht eh schon ConditionalMoves ausspuckt, was Du am ASM überprüfen müsstest
Tut keiner, sagt Godbolt.
Schrompf hat geschrieben:
20.06.2019, 11:19
Die Chancen stehen gut, dass der Compiler dann eh schon zu vektorisieren anfängt. […] Der Compiler dürfte die sogar benutzen, wenn Du ihm die richtigen arch-Flags mitgibst.
GCC nicht einmal mit -O3 und VC++ mit /arch:avx findet zwei Gründe gegen Vektorisierung, die ich jetzt nicht nachgeschlagen habe: https://godbolt.org/z/MBXP4h

Beide Compiler spucken um die 150 Befehle aus wo ich mir sicher bin, dass das Optimum bei unter 40 liegt.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
Schrompf
Moderator
Beiträge: 3905
Registriert: 26.02.2009, 00:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Schrompf » 20.06.2019, 14:39

Oh, wow. Ich hab nur meinen GameDev-Code als Referenz, und meine simple 3xfloat-Vektorklasse wird von allen Compilern quasi durchweg vektorisiert. Aber ich vermute, sobald ich Compares einbaue, ist auch Ende Gelände mit der Automatisierung.
Häuptling von Dreamworlds. Baut an was Neuem. Hilft nebenbei nur höchst selten an der Open Asset Import Library mit.

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 20.06.2019, 19:01

Huhu Leute!

@Schrompf:
Über fixed point arithmetic habe ich auch schon nachgedacht, aber mir kam der Gedanke, dass das irgentwie 1998 ist. Damals habe ich das gemacht, aber dann habe ich mal "gelernt" (und damit meine ich, dass ich ein allgemein gehaltenes Statement im "Internet" gelesen habe), dass man das "heutzutage" nicht mehr machen muss/sollte. Vielleicht probiere ich das auch mal bei Gelegenheit aus.

@Kristhy:
Ich habe mal fix nebenbei auf deinen ersten skalaren Ansatz umgestellt und nutze mal __m128 und die entsprechenden Funktionen.
Sonst keine Anpassungs (nix aligned, nix vektorisiert).
Ich messe bei meinem Referenzvideo auf dem Rechner, auf dem ich gerade bin, eine Änderung von alt ~8,2ms/Bild zu ~5,0ms/Bild.
Allein schon das entschäft die Situation ziemlich (keine Sprünge/ifs mehr -> Clipping verbessert, keine Konvertierungen) und lässt Potential erahnen. Beeindruckend.

Ich versuche später (die Tage/heute Abend) mal was zu bauen, um mehrere Pixel auf einmal durch die Mühle zu drehen.

Aber bisher schonmal Danke an Euch Beide: Das sind HERVORRAGENDE Denkansätze für mich. Ich hatte das Thema/Potential bisher sehr unterschätzt.

Ich melde mich, wenn es mehr zu erzählen gibt.

Viele Güße,
Top-OR
--
Verallgemeinerungen sind IMMER falsch.

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 21.06.2019, 14:18

So, da bin ich wieder!

Also es ist leider dabei geblieben. Ich habe den Code nun in einer skalaren Form mit SSE. So siehts gerade aus - schonmal deutschlich kompakter und wie gesagt, auch deutlich schneller.

Das Ganze ist in der Form nun auch auf Systemen, die vorher bei Videos bestimmter Auflösung völlig versagt haben, nun auch ausführbar. Gerade auf einem schwachen Test-System (Pentium N4200) ist der Gewindigkeitszuwachs hierdurch schon recht gross geworden: ~30ms -> ~10ms. Cool cool!

Ich tue mich jedoch sehr schwer, den Code tatsächlich zu vektorisieren. Momentan sieht er so aus:

Code: Alles auswählen

static const __m128 CONST_1_140 = _mm_set_ss(1.140f);
static const __m128 CONST_0_395 = _mm_set_ss(0.395f);
static const __m128 CONST_0_581 = _mm_set_ss(0.581f);
static const __m128 CONST_2_032 = _mm_set_ss(2.032f);
static const __m128 CONST_255_0 = _mm_set_ss(255.0f);
static const __m128 CONST_0_0   = _mm_set_ss(0.0f);



void convertPlanesYUV420pToBGRWithSSE(const unsigned char* PlaneY, const unsigned char* PlaneU, unsigned char hsByte* PlaneV, unsigned char* TargetBuffer, int ResX, int ResY)
{
    const int _HalfResX(ResX/2);

    for (int CntY=0; CntY<ResY; CntY++) {

        const int _HalfLineYMulHalfResX( (CntY/2) * _HalfResX);
        const int _ResXMulLineY(CntY * ResX);

        for (int CntX=0; CntX<ResX; CntX++) {
            const hsLongInt _ColorPlaneIndex (_HalfLineYMulHalfResX + (CntX/2) );

            const __m128 _ValY = _mm_cvtepi32_ps(_mm_cvtsi32_si128(PlaneY[_ResXMulLineY + CntX] - 16));
            const __m128 _ValU = _mm_cvtepi32_ps(_mm_cvtsi32_si128(PlaneU[_ColorPlaneIndex] - 128));
            const __m128 _ValV = _mm_cvtepi32_ps(_mm_cvtsi32_si128(PlaneV[_ColorPlaneIndex] - 128));

            /// convert the blue component
            __m128 _Blue = _mm_add_ss(_ValY, _mm_mul_ss(CONST_1_140, _ValU) ) ;
            _Blue = _mm_min_ss(_Blue, CONST_255_0);
            _Blue = _mm_max_ss(_Blue, CONST_0_0);
            *TargetBuffer++ = static_cast<unsigned char>(_mm_cvt_ss2si(_Blue));

            /// convert the green component
            __m128 _Green = _mm_sub_ss(_mm_sub_ss(_ValY, _mm_mul_ss(CONST_0_395, _ValU)  ), _mm_mul_ss(CONST_0_581, _ValV));
            _Green = _mm_min_ss(_Green, CONST_255_0);
            _Green = _mm_max_ss(_Green, CONST_0_0);
            *TargetBuffer++ = static_cast<unsigned char>(_mm_cvt_ss2si(_Green));

            /// convert the red component
            __m128 _Red = _mm_add_ss(_ValY, _mm_mul_ss(CONST_2_032, _ValV) );
            _Red = _mm_min_ss(_Red, CONST_255_0);
            _Red = _mm_max_ss(_Red, CONST_0_0);
            *TargetBuffer++ = static_cast<unsigned char>(_mm_cvt_ss2si(_Red));
        }
    }
}

Also, hier mal meine Gedanken, wie ichs verstanden habe, was passiert und was nun getan werden muss. Bitte korrigieren, falls es ein Mißverständnis gab:

Der obige Code nutzt die Datentyp __m128, welcher Platz für 4 32bit floats bietet. So, wie er jetzt da steht, werden einzelne Float-Skalare in die untere Komponente von __m128 geladen, die anderen 3 werden anfangs auf Null gesetzt.
Dann mache ich meine Rechenoperationen, die dann im Grunde immer nur die untere Komponente von __m128 bearbeiten und die anderen 3 leeren Stellen (mehr oder weniger effizient) mit durchschleifen.

Doch es wäre ja cool, wenn man einen voll beladenen __m128 nutzt. Die Idee wäre nun, immer 4 Pixel auf statt einen auf einmal auf diese Multiplikations- und Additionsreise zu schicken. Und ja, ich kann (width % 4 == 0) garantieren.

Jetzt kommt die Prasix (kein klingonischer Mond, aber ähnlich schwieriger Boden):
Ausgangspunkte der Rechenorgie für jeden Pixel sind ja die drei Werte _ValY, _ValU und _ValV. Die ermittle ich durch einen Lookup auf jeweils ein unsigned char*-Buffer. Diesen Lookup macht der obige Code ja für einen Wert/Pixel/Plane (-> short) und diesem Short wandeln wir mit den zwei Funktionen _mm_cvtsi32_si128 und _mm_cvtepi32_ps in einen Float und packen ihn in die "untere Komponente" von __m128.

Oder mal für eine Plane gesprochen ist es gerade so: KonvertiereIntM128ZuFloatM128(PackeIntegerInM128(Plane[_PlaneIndex]))

Jetzt mein Gedanke, wie es weitergeht:
Ich mache den Lookup für das Plane Value (z.B. _ValY) für 4 aufeinaderfolgende Pixel (Short) und speichere das Ergebnis in einem lokalen Buffer Short[4].

Wie bekomme ich dieses Short[4] auf einen Rutsch in ein __M128?

Irgendwie stecke ich da gerade fest . Die Doku von Intel (https://software.intel.com/sites/landin ... vtepi32_ps) finde ich ganz nett, ich weiss aber nicht, wonach ich genau suchen muss.

Oder kann ich das ganze irgendwie direkt auf dem Planebuffers (siehe Funktionsparameter: const unsigned char*) machen, ohne den Umweg über ein lokales Short[4] zu gehen?

Vielen Dank schonmal,
Top-OR
--
Verallgemeinerungen sind IMMER falsch.

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 21.06.2019, 16:51

Später lange Antwort jetzt erstmal 4×short laden:

Code: Alles auswählen

NO_SIDE_EFFECTS SInt2Bx4 SIMD_CALL loadx4(SInt2B const memory[]) {
	// • load one 64-bit integer with all four 16-bit values via SSE’s MOVQ
#	if COMPILED_BY_VISUAL_CPP
		// • MOVQ is available via “_mm_loadl_epi64()” intrinsic
		return { _mm_loadl_epi64(reinterpret_cast<SInt4B const *>(memory)) };
#	elif COMPILED_BY_CLANG
		// • Clang generates MOVQ/MOVSQ for this code
		return { IMPL_SINT2BX4{ memory[0], memory[1], memory[2], memory[3] } };
#	elif COMPILED_BY_GCC
		// • GCC generates MOVQ/MOVSQ only either for “__builtin_memcpy()” or for very ugly “union” code
		IMPL_SINT2BX4 abcd = { 0 };
		__builtin_memcpy(&abcd, memory, 8);
		return { abcd };
#	endif
}
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 22.06.2019, 12:08

So, bevor das Familienwocheende seinen Gang geht, habe ich versucht, noch ein bisschen zu basteln.

Ich ziehe jetzt 4 Pixel auf einmal durch die Multiplikationshölle.

Leider muss ich am Ende wieder Byteweise schreiben, weil ich von einer Logik, die Color-Planes/Kanäle nacheinander vearbeitet ja irgentwann in den Modus kommen muss, wo Pixel (die Ihrerseits aus Farbcomponenten bestehen) nacheinander kommen.

Code: Alles auswählen

static const __m128 CONST_1_140 = _mm_set_ps1(1.140f);
static const __m128 CONST_0_395 = _mm_set_ps1(0.395f);
static const __m128 CONST_0_581 = _mm_set_ps1(0.581f);
static const __m128 CONST_2_032 = _mm_set_ps1(2.032f);
static const __m128 CONST_255_0 = _mm_set_ps1(255.0f);
static const __m128 CONST_0_0   = _mm_set_ps1(0.0f);

void convertPlanesYUV420pToBGRWithSSE(const unsigned char* PlaneY, const unsigned char* PlaneU, const unsigned char* PlaneV, unsigned char* TargetBuffer, int ResX, int ResY)
{
    const int _HalfResX(ResX/2);
    const int _QuaterResX(ResX/4);

    for (int CntY=0; CntY<ResY; CntY++) {

        const int _HalfLineYMulHalfResX( (CntY/2) * _HalfResX);
        const int _ResXMulLineY(CntY * ResX);

        for (int CntX=0; CntX<ResX; CntX+=4) {

            /// prepare vector of ValY
            const __m128 _ValY = _mm_cvtepi32_ps( _mm_set_epi32(PlaneY[_ResXMulLineY + CntX    ] - 16, PlaneY[_ResXMulLineY + CntX + 1] - 16,  PlaneY[_ResXMulLineY + CntX + 2] - 16,     PlaneY[_ResXMulLineY + CntX + 3] - 16));

            /// prepare indices for the color planes fetch
            const long int _ColorPlaneIndexOne (_HalfLineYMulHalfResX + ((CntX  )   / 2));
            const long int _ColorPlaneIndexTwo (_HalfLineYMulHalfResX + ((CntX + 2) / 2));

            /// prepare vector of ValU
            const short int _U1(PlaneU[_ColorPlaneIndexOne] - 128);
            const short int _U2(PlaneU[_ColorPlaneIndexTwo] - 128);
            const __m128 _ValU = _mm_cvtepi32_ps( _mm_set_epi32(_U1, _U1, _U2, _U2)  );

            /// prepare vector of ValV
            const short int _V1(PlaneV[_ColorPlaneIndexOne] - 128);
            const short int _V2(PlaneV[_ColorPlaneIndexTwo] - 128);
            const __m128 _ValV = _mm_cvtepi32_ps( _mm_set_epi32(_V1, _V1, _V2, _V2)  );

            /// convert the blue component
            __m128 _Blue = _mm_add_ps(_ValY, _mm_mul_ps(CONST_1_140, _ValU) ) ;
            _Blue = _mm_min_ps(_Blue, CONST_255_0);
            _Blue = _mm_max_ps(_Blue, CONST_0_0);
            short int _BlueValues[4];
            _mm_store_si128((__m128i*)_BlueValues, _mm_cvtps_epi32(_Blue));

            /// convert the green component
            __m128 _Green = _mm_sub_ps(_mm_sub_ps(_ValY, _mm_mul_ps(CONST_0_395, _ValU)  ), _mm_mul_ps(CONST_0_581, _ValV));
            _Green = _mm_min_ps(_Green, CONST_255_0);
            _Green = _mm_max_ps(_Green, CONST_0_0);
            short int _GreenValues[4];
            _mm_store_si128((__m128i*)_GreenValues, _mm_cvtps_epi32(_Green));

            /// convert the red component
            __m128 _Red = _mm_add_ps(_ValY, _mm_mul_ps(CONST_2_032, _ValV) );
            _Red = _mm_min_ps(_Red, CONST_255_0);
            _Red = _mm_max_ps(_Red, CONST_0_0);
            short int _RedValues[8];
            _mm_store_si128((__m128i*)_RedValues, _mm_cvtps_epi32(_Red));


            /// write colors the the output buffer and convert planes to component-based-pixels (change color interlacing)
            *TargetBuffer++ = _BlueValues[6];
            *TargetBuffer++ = _GreenValues[6];
            *TargetBuffer++ = _RedValues[6];

            *TargetBuffer++ = _BlueValues[4];
            *TargetBuffer++ = _GreenValues[4];
            *TargetBuffer++ = _RedValues[4];

            *TargetBuffer++ = _BlueValues[2];
            *TargetBuffer++ = _GreenValues[2];
            *TargetBuffer++ = _RedValues[2];

            *TargetBuffer++ = _BlueValues[0];
            *TargetBuffer++ = _GreenValues[0];
            *TargetBuffer++ = _RedValues[0];
        }
    }
}
Ich glaube, diese Implementierung hat an den Stellen noch Probleme, wo ich in die SS4-Welt rein und wieder rauskomme.

Also z.B. 1) hier

Code: Alles auswählen

            /// prepare vector of ValU
            const short int _U1(PlaneU[_ColorPlaneIndexOne] - 128);
            const short int _U2(PlaneU[_ColorPlaneIndexTwo] - 128);
            const __m128 _ValU = _mm_cvtepi32_ps( _mm_set_epi32(_U1, _U1, _U2, _U2)  );
und 2) hier

Code: Alles auswählen

 ....
	short int _RedValues[8];
	_mm_store_si128((__m128i*)_RedValues, _mm_cvtps_epi32(_Red));
Ich glaube, gerade bei 2 wird eine Kopie angefertigt, die ich nicht brauche, aber ich habe noch keine andere Möglichkeit gefunden, danach dann sowas zu machen, als wären es nochmale Integers (hier _BlueValues)

Code: Alles auswählen

            *TargetBuffer++ = _BlueValues[4];



Trotzdem senkt diese Implementierung die Runtime der Funktion nochmal von ~5ms auf <2ms und funktioniert einwandfrei. Coole Sache!

EDIT: Ups, mit dem falschen Rechner gebenchmarkt. Es sind ~3ms nun.
--
Verallgemeinerungen sind IMMER falsch.

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 22.06.2019, 15:35

Da kann man auf jeden Fall noch mehr rauskitzeln.

Eine Sache die auf jeden Fall geht wäre auf 16 Bit Integer zu gehen via _mm_mullo_epi16, _mm_mulhi_epi16 und _mm_madd_epi16. Man kann dann ja doppelt so viele Daten pro SIMD-Befehl verarbeiten. Wenn ich das gerade richtig sehe, werden im Moment auch die selben U und V Werte ja 4 mal einzeln geladen und 4 mal einzeln multipliziert. Das kann man auch vermeiden.

Auch beim Laden der Daten, dass du selbst schon angesprochen hast, kann man noch was rausholen. Insbesondere wenn man die obigen Vorschläge umsetzt, ist das sicher ein riesen Bottleneck. Ich würde versuchen, größere Blöcke auf einmal zu laden und dann mit Shuffles/Unpack die Bytes in die richtige Position bringen. Wenn SSSE3 verfügbar ist (Ich denke das kann man heutzutage auch langsam einfach voraussetzen) gibts auch _mm_shuffle_epi8.

Noch zwei Dinge:
  • _mm_store_si128 benötigt ausgerichteten Speichern. Du solltest die _RedValues Variablen mit alignas(16) markieren, sonst gibt es einen Fehler wenn der Stack nicht zufällig schon richtig ausgerichtet ist.
  • Der C++-Standard ist nicht mit deinen Variablennamen einverstanden: "Each name that [...] begins with an underscore followed by an uppercase letter is reserved to the implementation for any use."
EDIT:
Ist das ganze eigentlich eher als 32 oder 64 Bit Anwendung gedacht?

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 22.06.2019, 16:56

Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Da kann man auf jeden Fall noch mehr rauskitzeln.
Das glaube ich auch! Aber ich bin über die jetzige Verbesserung schon hocherfreut! :-D
Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Eine Sache die auf jeden Fall geht wäre auf 16 Bit Integer zu gehen via _mm_mullo_epi16, _mm_mulhi_epi16 und _mm_madd_epi16. Man kann dann ja doppelt so viele Daten pro SIMD-Befehl verarbeiten.
Ich gucks mir mal an. Momemtan hat mich die RIESIGE Liste von SSE Befehlen erstmal erschlagen und ich muss schauen, was es da alles so gibt. Danke hierfür.
Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Wenn ich das gerade richtig sehe, werden im Moment auch die selben U und V Werte ja 4 mal einzeln geladen und 4 mal einzeln multipliziert. Das kann man auch vermeiden.
Öm, nö ?!? .. sie werden je einmal gefetcht (U1, U2, V1, V2) und dann jeweils 2x in einen __m128 gequetscht. Ok, das 4x -128 direkt nach dem fetch könnte man noch im SSE-Universum machen. Das liegt daran, dass jede zweite Spalte gleich die gleiche U und V Komponente hat, da die Planes nur halb so gross sind. Wie würdest du das umsetzen? Ich würde an der Stelle diesen Effekt nicht optimieren wollen und mal so tun wollen, als ob die Ausgangsvariablen (Indices und die Results der U/V-Fetches) für jeden Pixel andere wären.
Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Auch beim Laden der Daten, dass du selbst schon angesprochen hast, kann man noch was rausholen. Insbesondere wenn man die obigen Vorschläge umsetzt, ist das sicher ein riesen Bottleneck.
Das glaube ich auch, aber ich bin froh, dass es erstmal überhaupt geht.
Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Ich würde versuchen, größere Blöcke auf einmal zu laden und dann mit Shuffles/Unpack die Bytes in die richtige Position bringen. Wenn SSSE3 verfügbar ist (Ich denke das kann man heutzutage auch langsam einfach voraussetzen) gibts auch _mm_shuffle_epi8.
Darüber weiss ich garnichts. Gibts da ne Familie von SSE-Befehlen für? Ich gucke ...

Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
  • _mm_store_si128 benötigt ausgerichteten Speichern. Du solltest die _RedValues Variablen mit alignas(16) markieren, sonst gibt es einen Fehler wenn der Stack nicht zufällig schon richtig ausgerichtet ist.
Ja, das stimmt. Haste Recht. Bin mir aber nicht sicher, ob der Zwischenschritt über _mm_store_si128 überhaupt nötig ist. Mir schweb vor, dass ich doch irgentwie direkter (über casts) an die Komponenten des __mm128i rankommen könnte. Oder?

Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
[*]Der C++-Standard ist nicht mit deinen Variablennamen einverstanden: "Each name that [...] begins with an underscore followed by an uppercase letter is reserved to the implementation for any use."[/list]
Hier tun wir mal so, als ob der Standard das mal nicht sieht. Huhu, hier, hier, ein Vögelchen ... da oben! [-> Ich werds mir merken]

Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
EDIT:
Ist das ganze eigentlich eher als 32 oder 64 Bit Anwendung gedacht?
Momentan noch 32bit mit MinGW unter Windows 64 ... mit 64Bit beschäftige ich mich später mal ...
--
Verallgemeinerungen sind IMMER falsch.

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 22.06.2019, 17:16

Top-OR hat geschrieben:
22.06.2019, 16:56
Ich gucks mir mal an. Momemtan hat mich die RIESIGE Liste von SSE Befehlen erstmal erschlagen und ich muss schauen, was es da alles so gibt. Danke hierfür.
Jo, absolut verständlich. :)
Macht aber auch irgendwie einen Reiz aus wenn man sich mal einen Überblick verschafft hat. Und zum Teil gibt es auch richtig komische Lücken.
Andererseits, macht es mir vlt. gerade deswegen SIMD-Code richtig viel Spaß. Es ist ein gutes Puzzle die richtigen Intrinsics zu finden. ^^
Top-OR hat geschrieben:
22.06.2019, 16:56
Öm, nö ?!? .. sie werden je einmal gefetcht (U1, U2, V1, V2) und dann jeweils 2x in einen __m128 gequetscht. Ok, das 4x -128 direkt nach dem fetch könnte man noch im SSE-Universum machen. Das liegt daran, dass jede zweite Spalte gleich die gleiche U und V Komponente hat, da die Planes nur halb so gross sind. Wie würdest du das umsetzen? Ich würde an der Stelle diesen Effekt nicht optimieren wollen und mal so tun wollen, als ob die Ausgangsvariablen (Indices und die Results der U/V-Fetches) für jeden Pixel andere wären.
Ich meinte damit auch, dass du die Daten ja für die erste Zeile und für die zweite Zeile ja nochmal lädst.
Man könnte einfach 2 Zeilen auf einmal verarbeiten und alles nur einmal laden/mulitiplizieren.
Top-OR hat geschrieben:
22.06.2019, 16:56
Spiele Programmierer hat geschrieben:
22.06.2019, 15:35
Ich würde versuchen, größere Blöcke auf einmal zu laden und dann mit Shuffles/Unpack die Bytes in die richtige Position bringen. Wenn SSSE3 verfügbar ist (Ich denke das kann man heutzutage auch langsam einfach voraussetzen) gibts auch _mm_shuffle_epi8.
Darüber weiss ich garnichts. Gibts da ne Familie von SSE-Befehlen für? Ich gucke ...
_mm_shuffle_epi8 ist im Prinzip die eierlegende Wollmilchsau. Damit kannst du die Bytes innerhalb eines SIMD Registers frei durchwechseln und z.B. auch die Positionierung in 3er Blöcken erreichen die du am Ende haben willst.
_mm_unpacklo_epi ist viel spezieller, aber trotzdem sehr oft nützlich. Damit kannst du z.B. Bytes in Shorts umwandeln. (In dem zu Nullen dazwischen packst.)
Top-OR hat geschrieben:
22.06.2019, 16:56
Ja, das stimmt. Haste Recht. Bin mir aber nicht sicher, ob der Zwischenschritt über _mm_store_si128 überhaupt nötig ist. Mir schweb vor, dass ich doch irgentwie direkter (über casts) an die Komponenten des __mm128i rankommen könnte. Oder?
Jaein. Es gibt _mm_cvtsi128_si32 um einen 32 Bit Integer aus einem SIMD-Register zu bekommen.
Aber das würde ich nur sehr bedingt empfehlen. Prinzipiell sollte es das Ziel sein, die komplette Verarbeitung in SSE-Registern hinzubekommen, und gar nicht erst Bytes einzeln in den Speicher schreiben.

Mich juckts gerade in den Fingern, wenn du nichts dagegen hast, werde ich mal ein paar meiner Vorschläge ausprobieren.

EDIT:
Fehler korrigiert, ich meinte natürlich _mm_cvtsi128_si32 nicht _mm_cvttsd_si32.
Zuletzt geändert von Spiele Programmierer am 22.06.2019, 17:22, insgesamt 1-mal geändert.

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 22.06.2019, 17:21

Spiele Programmierer hat geschrieben:
22.06.2019, 17:16
Mich juckts gerade in den Fingern, wenn du nichts dagegen hast, werde ich mal ein paar meiner Vorschläge ausprobieren.
Nur zu!

Ich finde das Thema gerade richtig spannend - es ist ein völlig neues Terrain für mich - konnte das immer irgendwie umschiffen oder es war nie so kritisch, dass ich Intrinsics benutzen wollte/sollte/musste, aber das Thema scheint ja, gerade bei bestimmten Problemen wie diesem, ziemlich viel Potential zu haben. Schon sehr interessant ...

Den Vergleich mit dem Puzzle habe ich auch schon in den letzten Tagen im Kopf gehabt ... Hähää


Edit: Achso ... ich kann nicht garantieren, dass die Funktionsparameter (YUV Planes, Targetbuffer) aligned sind. Die können irgendwo im Heap liegen.
--
Verallgemeinerungen sind IMMER falsch.

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 22.06.2019, 18:31

Ok habe bisschen rumgebastellt und massiv die Performance verbessert.
In Zahlen in meinen (sehr provisorischen) Benchmark:
- Ganz ursprünglich: ~600 ms
- Ursprüngliche SSE Version: ~100 ms
- Verbesserte SSE Version: ~12 ms

Hier der relevante Code:

Code: Alles auswählen

#include <emmintrin.h>
#include <tmmintrin.h>

// Factors by which the YUV components are mixed. (In 9.6 fixed point format)
static const __m128i RedVFactor = _mm_set1_epi16(static_cast<short>(2.032 * 64 + 0.5));
static const __m128i GreenUFactor = _mm_set1_epi16(static_cast<short>(0.395 * 64 + 0.5));
static const __m128i GreenVFactor = _mm_set1_epi16(static_cast<short>(0.581 * 64 + 0.5));
static const __m128i BlueUFactor = _mm_set1_epi16(static_cast<short>(1.140 * 64 + 0.5));

// Masks that move the corresponding bytes into the right position.
static const __m128i ShuffleRed = _mm_setr_epi8(10, 5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15);
static const __m128i ShuffleGreen = _mm_setr_epi8(5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10);
static const __m128i ShuffleBlue = _mm_setr_epi8(0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5);
static const __m128i Select0 = _mm_setr_epi8(-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1);
static const __m128i Select1 = _mm_setr_epi8(0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0);
static const __m128i Select2 = _mm_setr_epi8(0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0);

void convertPlanesYUV420pToBGRWithSSEVersion2(unsigned char const* PlaneY, unsigned char const* PlaneU, unsigned char const* PlaneV, unsigned char* TargetBuffer, int ResX, int ResY) {
	// The following code works with 16 by 2 blocks of pixels at once.
	static const int StepSizeX = 16;
	static const int StepSizeY = 2;
	assert((ResX % StepSizeX) == 0); // I hope this is given and must not be handled?
	assert((ResY % StepSizeY) == 0); // I hope this is given and must not be handled?
	for (int CntY = 0; CntY < ResY; CntY += StepSizeY) {
		const unsigned char* const CurrentPlaneY = PlaneY + CntY * ResX;
		const unsigned char* const CurrentPlaneU = PlaneU + (CntY / 2) * (ResX / 2);
		const unsigned char* const CurrentPlaneV = PlaneV + (CntY / 2) * (ResX / 2);
		unsigned char* const CurrentTargetBuffer = TargetBuffer + CntY * (ResX * 3);
		for (int CntX = 0; CntX < ResX; CntX += StepSizeX) {

			// Load the data...
			const __m128i LoadedY0 = _mm_loadu_si128(reinterpret_cast<__m128i const*>(CurrentPlaneY + CntX)); // Load 16 bytes
			const __m128i LoadedY1 = _mm_loadu_si128(reinterpret_cast<__m128i const*>(CurrentPlaneY + CntX + ResX)); // Load 16 bytes
			const __m128i LoadedU = _mm_loadl_epi64(reinterpret_cast<__m128i const*>(CurrentPlaneU + CntX / 2)); // Load 8 bytes
			const __m128i LoadedV = _mm_loadl_epi64(reinterpret_cast<__m128i const*>(CurrentPlaneV + CntX / 2)); // Load 8 bytes

			// Bring it into 16 bit ints and adjust the format to
			// (1 bit sign, 9 bits integral, 6 bits fractional).
			// The integral part is a little bigger than usual to avoid intermediate overflow of the multiplication or the clamping.
			const __m128i Zero = _mm_setzero_si128();
			const __m128i Y00 = _mm_sub_epi16(_mm_slli_epi16(_mm_unpacklo_epi8(LoadedY0, Zero), 6), _mm_set1_epi16(16 << 6)); // First 8 shorts of first row.
			const __m128i Y10 = _mm_sub_epi16(_mm_slli_epi16(_mm_unpackhi_epi8(LoadedY0, Zero), 6), _mm_set1_epi16(16 << 6)); // Second 8 shorts of first row.
			const __m128i Y01 = _mm_sub_epi16(_mm_slli_epi16(_mm_unpacklo_epi8(LoadedY1, Zero), 6), _mm_set1_epi16(16 << 6)); // First 8 shorts of second row.
			const __m128i Y11 = _mm_sub_epi16(_mm_slli_epi16(_mm_unpackhi_epi8(LoadedY1, Zero), 6), _mm_set1_epi16(16 << 6)); // Second 8 shorts of second row.
			const __m128i U = _mm_sub_epi16(_mm_unpacklo_epi8(LoadedU, Zero), _mm_set1_epi16(128)); // Don't shift them yet. They will be shifted by the multiplication by the *Factors.
			const __m128i V = _mm_sub_epi16(_mm_unpacklo_epi8(LoadedV, Zero), _mm_set1_epi16(128)); // Don't shift them yet. They will be shifted by the multiplication by the *Factors

			// Calculate resulting red components...
			const __m128i RedAdjust = _mm_mullo_epi16(V, RedVFactor);
			const __m128i R00 = _mm_adds_epi16(Y00, _mm_unpacklo_epi16(RedAdjust, RedAdjust));
			const __m128i R10 = _mm_adds_epi16(Y10, _mm_unpackhi_epi16(RedAdjust, RedAdjust));
			const __m128i R01 = _mm_adds_epi16(Y01, _mm_unpacklo_epi16(RedAdjust, RedAdjust));
			const __m128i R11 = _mm_adds_epi16(Y11, _mm_unpackhi_epi16(RedAdjust, RedAdjust));

			// Calculate resulting green components...
			const __m128i GreenAdjust = _mm_adds_epi16(_mm_mullo_epi16(U, GreenUFactor), _mm_mullo_epi16(V, GreenVFactor));
			const __m128i G00 = _mm_subs_epi16(Y00, _mm_unpacklo_epi16(GreenAdjust, GreenAdjust));
			const __m128i G10 = _mm_subs_epi16(Y10, _mm_unpackhi_epi16(GreenAdjust, GreenAdjust));
			const __m128i G01 = _mm_subs_epi16(Y01, _mm_unpacklo_epi16(GreenAdjust, GreenAdjust));
			const __m128i G11 = _mm_subs_epi16(Y11, _mm_unpackhi_epi16(GreenAdjust, GreenAdjust));

			// Calculate resulting blue components...
			const __m128i BlueAdjust = _mm_mullo_epi16(U, BlueUFactor);
			const __m128i B00 = _mm_adds_epi16(Y00, _mm_unpacklo_epi16(BlueAdjust, BlueAdjust));
			const __m128i B10 = _mm_adds_epi16(Y10, _mm_unpackhi_epi16(BlueAdjust, BlueAdjust));
			const __m128i B01 = _mm_adds_epi16(Y01, _mm_unpacklo_epi16(BlueAdjust, BlueAdjust));
			const __m128i B11 = _mm_adds_epi16(Y11, _mm_unpackhi_epi16(BlueAdjust, BlueAdjust));

			// Compress shorts back into bytes.
			// (Also does the clamping.)
			const __m128i OutR0 = _mm_packus_epi16(_mm_srai_epi16(R00, 6), _mm_srai_epi16(R10, 6)); // First row, To do rounding, add 32 before shifting.
			const __m128i OutR1 = _mm_packus_epi16(_mm_srai_epi16(R01, 6), _mm_srai_epi16(R11, 6)); // Second row
			const __m128i OutG0 = _mm_packus_epi16(_mm_srai_epi16(G00, 6), _mm_srai_epi16(G10, 6));
			const __m128i OutG1 = _mm_packus_epi16(_mm_srai_epi16(G01, 6), _mm_srai_epi16(G11, 6));
			const __m128i OutB0 = _mm_packus_epi16(_mm_srai_epi16(B00, 6), _mm_srai_epi16(B10, 6));
			const __m128i OutB1 = _mm_packus_epi16(_mm_srai_epi16(B01, 6), _mm_srai_epi16(B11, 6));

			// Now we need to write these values out in the correct order.
			// First bring the bytes into their final positions.
			const __m128i RedBytes0 = _mm_shuffle_epi8(OutR0, ShuffleRed);
			const __m128i GreenBytes0 = _mm_shuffle_epi8(OutG0, ShuffleGreen);
			const __m128i BlueBytes0 = _mm_shuffle_epi8(OutB0, ShuffleBlue);
			const __m128i RedBytes1 = _mm_shuffle_epi8(OutR1, ShuffleRed);
			const __m128i GreenBytes1 = _mm_shuffle_epi8(OutG1, ShuffleGreen);
			const __m128i BlueBytes1 = _mm_shuffle_epi8(OutB1, ShuffleBlue);

			// Now choose 1 red byte, 1 green byte and 1 blue byte in an alternating fashion.
			// I also tried using "_mm_blendv_epi8" from SSE4.1, but it's actually slower by 15% overall.
			const __m128i Out0 = _mm_or_si128(_mm_and_si128(BlueBytes0, Select0), _mm_or_si128(_mm_and_si128(GreenBytes0, Select1), _mm_and_si128(RedBytes0, Select2))); // Now choose 1 red byte, 1 green byte and 1 blue byte in an alternating fashion.
			const __m128i Out1 = _mm_or_si128(_mm_and_si128(BlueBytes0, Select2), _mm_or_si128(_mm_and_si128(GreenBytes0, Select0), _mm_and_si128(RedBytes0, Select1)));
			const __m128i Out2 = _mm_or_si128(_mm_and_si128(BlueBytes0, Select1), _mm_or_si128(_mm_and_si128(GreenBytes0, Select2), _mm_and_si128(RedBytes0, Select0)));
			const __m128i Out3 = _mm_or_si128(_mm_and_si128(BlueBytes1, Select0), _mm_or_si128(_mm_and_si128(GreenBytes1, Select1), _mm_and_si128(RedBytes1, Select2))); // Now choose 1 red byte, 1 green byte and 1 blue byte in an alternating fashion.
			const __m128i Out4 = _mm_or_si128(_mm_and_si128(BlueBytes1, Select2), _mm_or_si128(_mm_and_si128(GreenBytes1, Select0), _mm_and_si128(RedBytes1, Select1)));
			const __m128i Out5 = _mm_or_si128(_mm_and_si128(BlueBytes1, Select1), _mm_or_si128(_mm_and_si128(GreenBytes1, Select2), _mm_and_si128(RedBytes1, Select0)));

			// Write the data to the target...
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + CntX * 3), Out0);
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + CntX * 3 + 16), Out1);
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + CntX * 3 + 32), Out2);
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + (CntX + ResX) * 3), Out3);
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + (CntX + ResX) * 3 + 16), Out4);
			_mm_storeu_si128(reinterpret_cast<__m128i*>(CurrentTargetBuffer + (CntX + ResX) * 3 + 32), Out5);
		}
	}
}
Der Code setzt jetzt SSSE3 für den Shuffle vorraus, aber das sollte inzwischen eigentlich jeder haben.
Ansonsten habe ich im Wesentlichen die Sachen umgesetzt, die ich vorgeschlagen habe.
Der Code rundet vereinzelt leicht anders, aber in meinem Test ergibt sich eine Abweichung von maximal 1, wobei in den meisten Fällen keine Abweichung besteht.

Falls jemand noch weiter experimentieren will, ich habe meinen vollständigen Testcode (mit Windows Funktionen) angehängt.
Dateianhänge
TestCode.cpp
(29.44 KiB) 20-mal heruntergeladen

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 22.06.2019, 19:26

Ich habe deine Methode mal bei mir eingebaut.
Ich musste SSE4.1 Flags setzen und #include <tmmintrin.h>, sonst gings nicht bei mir - sicher, dass es SSE3 ist?

SCHEISS DIE WAND AN: Auf meinem Referenz-Benchmark-System mit dem Referenz-Video komme ich jetzt auf ~0.6ms ... Meine Güte! Krass!
Selbst auf den schwächsten Systemen, die ich hier zum Testen so nutze (Pentium N4200 und Laptop mit Core i7 [<- stark gedrosselt ohne Netzstrom]) laufen jetzt sogar FullHD Videos. WOW! Da war ich mit der ganz anfänglichen Implementierung des YUV-Konverters teilweise im mittleren dreistellingen Millisekundenbereich.

Ich versuche die Tage mal zu verstehen, was du da genau getrieben hast. Aber ich glaube, diese Methode kann ich wohl als gute Referenz nehmen, um daraus zu lernen:

Schon oben bei Krishtys Posts dachte ich an die Stelle in Terminator 2, wo die Wissenschaftler den Arm und den Chip aus dem ersten T800 analysieren - keine Ahnung haben, was Sie da tun - aber Sie lassen sich davon inspirieren. Son bisschen habe ich gerade ein ähnliches Gefühl: Viel Potential!

EDIT:
Eine Frage: Wie ich sagte, kann ich nicht garantieren, dass die Source- und der Targetbuffer alligned sind. Läuft es nur "zufällig" ohne Absturz, sind die Buffers vielleicht implizit aligend worden (ohne, dass ich es weiss) oder ist das den SIMD Routinen schlicht in dieser Version egal?
--
Verallgemeinerungen sind IMMER falsch.

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 22.06.2019, 19:33

Freut mich wenn es bei dir auch so flott läuft. :-).

Und ich denke du musst SSSE3 anschalten, nicht SSE3.

Falls du es noch nicht kennst, der "Intrinsic-Browser" von Intel ist übrigens sehr nützlich: https://software.intel.com/sites/landin ... =undefined. Da steht u.a. auch welche SSSE Version und Header man braucht.

EDIT:
Bezüglich Alignment, dass passt wenn man die storeu und loadu Funktionen verwendet (was ich gemacht habe).
Potentiell sind die Funktionen ohne u wohl schneller, aber angeblich (d.h. ich habs ehrlich gesagt noch nicht selbst gemessen) ist der Unterschied nicht so groß.

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 22.06.2019, 19:59

Spiele Programmierer hat geschrieben:
22.06.2019, 17:16
Andererseits, macht es mir vlt. gerade deswegen SIMD-Code richtig viel Spaß. Es ist ein gutes Puzzle die richtigen Intrinsics zu finden. ^^
Ganz genau das!

Sehr schöner Code. Ich bin leider noch nicht zu meiner Optimierung gekommen (ich möchte ganz gern das Skalarprodukt ausprobieren), aber allein dein Rahmen-Code für den Vergleich mit den anderen Versionen wird mir da schon viel Zeit sparen :)
Spiele Programmierer hat geschrieben:
22.06.2019, 19:33
Potentiell sind die Funktionen ohne u wohl schneller, aber angeblich (d.h. ich habs ehrlich gesagt noch nicht selbst gemessen) ist der Unterschied nicht so groß.
Auf neuen CPUs gibt’s nur noch einen Unterschied, wenn das Laden/Speichern die Grenze einer Cache Line überquert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
marcgfx
Establishment
Beiträge: 1353
Registriert: 18.10.2010, 23:26

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von marcgfx » 23.06.2019, 08:46

Boa, ich seh da gibts viel zu lernen! Echt spannend der Thread. Habe ein paar instruktionen gegoogelt, weil ich nix verstehe und bin per Zufall auf das hier gestossen: https://www.ignorantus.com/yuv2rgb_sse2/
evtl. hats da was spannendes drin.

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 23.06.2019, 12:35

Sieht ziemlich ähnlich zu meinem Code aus. Inbesondere auch 16*2 Pixel auf einmal und 9.6 Festkomma.
Unterschied ist, dass ich in meinem Code am Ende ja noch die ganze Sache mit dem Shuffle machen musste, weil Top-OR die RGB-Werte am Ende Interleaved abgelegt haben wollte.

Außerdem:
Then it's time to prepare the uv factors that are common on both rows. Since using SSE2 multipliers is cheap, I expand the u and v data first and use 8 mullo instead of the obvious 4. Expanding afterwards is costly. I tried it.
Das hatte ich anders gemacht, in meinen Code wird nur einmal multipliziert und dann expandiert.

Benutzeravatar
Schrompf
Moderator
Beiträge: 3905
Registriert: 26.02.2009, 00:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Schrompf » 24.06.2019, 08:59

Sehr cool, den Thread merk ich mir für später. Die interaktive Übersicht von Intel ist schon ganz cool, aber gibt's die irgendwo auch mit nem Einzeiler, was die Funktion eigentlich tut? Ich habe noch kein Auge dafür entwickelt, um diese kryptischen Buchstabenmassaker in den Befehlsnamen direkt lesen zu können. Und ich habe jenseits von "Vektor-Add" eigentlich keine Ahnung, was für Befehle es überhaupt gibt.
Häuptling von Dreamworlds. Baut an was Neuem. Hilft nebenbei nur höchst selten an der Open Asset Import Library mit.

Benutzeravatar
Top-OR
Establishment
Beiträge: 330
Registriert: 02.03.2011, 17:32
Echter Name: Jens H.
Wohnort: Esslingen/Dessau
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Top-OR » 24.06.2019, 18:29

Also ich denke, es lohnt sich (für mich) inhaltlich nicht mehr, zu versuchen, diese Methode zu optimieren: Die ist mehr als gut/schnell genug.

Ich werde die Methode dauerhaft in meinen Code assimilieren und Spiele Programmierer/Krishty im Code als Kommentar und im Commit Comment ein paar nette Credits inkl. Link zum Thread hinterlassen. Zukunfts-Jens wird sich erinnern, wem er das zu verdanken hat ...
Ich lasse den alten Code (Basic SSE Version) als Fallback für nicht 4*2-konforme Bilder da.

Diese Intel-Referenz kannte ich, trotzdem muss man ganz schön rundern, um bei dem Kruscht an Bezeichnungen die Funktion zu finden, die man braucht: So, wie Schrompf es auch schon angedeutet hat. Ich bemerke aber mehr und mehr, dass sich bei der Namensgebung der Anweisungen ein paar Muster erkennen lassen - lesen kann ichs schon ganz ok, nur artikulieren könnte ich meine Wünsche noch nicht so, dass ich schnell die richtige Anweisung finde. Na, kommt sicher noch ...

Ich habe auch versucht, zu verstehen, was du getrieben hast. Wie schon teilweise erwähnt, kann ich folgende Grundgedanken nachvollziehen - ich versuche mal aufzulisten:

* 8 16Bit Pixel passen in eine 128bit SSE Struktur
* daher: Handling der Pixel zu je 4 Stück bei zwei Zeilen -> 8 Pixel auf einmal
* fetchen und schreiben der Daten direkt aus den Quell- und Zielbuffern ist mit alignment agnostischen Anweisungen möglich
* Verarbeitung mit fixed point math ist trotz SSE Anweisungen hilfreich und u.U. sinnvoll
* aus (und einpacken [unpack]) von Byte-Komponenten innerhalb der SSE Datenstruktur ist (begrenzt?) möglich
* Konvertierung von 16bit ints zu 8bit ints sind inkl. clamping durch spezialisierte Anweisungen möglich
* zurückschreiben der Register in den Speicher in einer nicht linearen Form ist durch Shuffling bitmaskenbasiert möglich

Also nochmal: Vielen Dank für die Inspiration und handfeste Hilfe. Vielleicht kann ich dieses Beispiel nutzen, um demnächst auch mal ein kleines Puzzle für mich selbst zu knacken. Ich werde berichten, wenn es dazu kommt (kann aber dauern).

Cool und Danke soweit,
Top-OR
--
Verallgemeinerungen sind IMMER falsch.

Benutzeravatar
Krishty
Establishment
Beiträge: 6974
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Krishty » 24.06.2019, 19:10

Schrompf hat geschrieben:
24.06.2019, 08:59
Ich habe noch kein Auge dafür entwickelt, um diese kryptischen Buchstabenmassaker in den Befehlsnamen direkt lesen zu können.
Top-OR hat geschrieben:
24.06.2019, 18:29
Ich bemerke aber mehr und mehr, dass sich bei der Namensgebung der Anweisungen ein paar Muster erkennen lassen
Ich habe auch keine Website mit Erklärungen parat, aber das Wichtigste für float:
_ss: scalar single (einzelnes float)
_ps: packed single (mehrere floats parallel)
_sd: scalar double (einzelnes double)
_pd: packed double (mehrere doubles parallel)

für int:
b: byte
w: word (16-Bit)
d: doubleword (32-bit)
q: quadword (64-bit)
u: unsigned (signed ist immer implizit)
s: saturation

Im Assembler wäre PACKUSWB also pack unsigned saturated bytes from words (also shorts von [-32768, +32767] in Bytes von [0, 255] stopfen)

Etc. Man gewöhnt sich dran. Und immer dran denken, dass man bei Godbolt über den Befehlen hovern kann …
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Spiele Programmierer
Establishment
Beiträge: 357
Registriert: 23.01.2013, 16:55

Re: YUV420planar zu BGR Konvertierung ist kritisch langsam

Beitrag von Spiele Programmierer » 24.06.2019, 20:16

Top-OR hat geschrieben:
24.06.2019, 18:29
Ich habe auch versucht, zu verstehen, was du getrieben hast. Wie schon teilweise erwähnt, kann ich folgende Grundgedanken nachvollziehen - ich versuche mal aufzulisten:

* 8 16Bit Pixel passen in eine 128bit SSE Struktur
* daher: Handling der Pixel zu je 4 Stück bei zwei Zeilen -> 8 Pixel auf einmal
* fetchen und schreiben der Daten direkt aus den Quell- und Zielbuffern ist mit alignment agnostischen Anweisungen möglich
* Verarbeitung mit fixed point math ist trotz SSE Anweisungen hilfreich und u.U. sinnvoll
* aus (und einpacken [unpack]) von Byte-Komponenten innerhalb der SSE Datenstruktur ist (begrenzt?) möglich
* Konvertierung von 16bit ints zu 8bit ints sind inkl. clamping durch spezialisierte Anweisungen möglich
* zurückschreiben der Register in den Speicher in einer nicht linearen Form ist durch Shuffling bitmaskenbasiert möglich
Jup, kann man so sagen.

Ansonsten ist es halt eine Sache einfach die richtigen Befehle zu finden.

Es gibt zwar schon eine gewisse Logik wie angedeutet, aber so wirklich toll ist die Benennung halt einfach nicht. Ich habe da leider auch keine bessere Referenz oder so. Die Befehle die häufig nützlich sind kennt man mit der Zeit und sonst ist der Browser gut um beispielsweise ein Stichwort einzugeben (z.B. shift).

Teilweise ist die Erklärung auch echt einfach schlecht, z.B. bei _mm_test_mix_ones_zeros:

Code: Alles auswählen

IF (a[127:0] AND mask[127:0] == 0) 
    ZF := 1 
ELSE 
    ZF := 0 
FI
IF ((NOT a[127:0]) AND mask[127:0] == 0) 
    CF := 1 
ELSE
    CF := 0
FI 
IF (ZF == 0 && CF == 0)
    dst := 1
ELSE 
    dst := 0
 FI
Hätte man auch mit ((a & mask) != 0) && ((~a & mask) != 0) zusammenfassen können.

Eine Sache wo man aufpassen muss ist bei _mm_set_. Dass geht nämlich genau andersrum als erwartet (wenn man es als Array-Elemente auffasst.). Die Funktion mit der "richtigen" Reihenfolge gibt es auch, heißt aber _mm_setr_. Habe schon Stunden mit Debugging verbraten um diesen Fehler zu finden.

Ein "Geheimtipp" ist noch _mm_movemask_epi8 (Die Benennung ist Katastrophe). Das hört sich komisch an, liefert aber einfach ein Bit aus jedem Byte. Sehr nützlich z.B. wenn man eine Verzweigungen im SIMD-Code nicht vermeiden kann.

Antworten