MMX Problem



  • Hallo,
    ich versuche gerade ein 16bit Bild etwas in der Helligkeit und Kontrast zu verändern und als 8bit Bild auszugeben. Ich hab das Problem, dass die normal C funktion etwas zu langsam ist und wollte daher diese Funktion unter Zuhilfename von MMX beschleunigen. Allerdings kenne ich mich mit Assembler und insbesondere mit MMX nicht aus und komme nicht weiter.
    Ich Versuche folgende Funktion umzusetzen:

    void AdjustContrast(unsigned char *dest, const unsigned short* src, unsigned long len,signed short B,signed short C)
    {
    	unsigned long x;
    	unsigned short temp;
    	for(x=0;x<len;x++){
    		//Saturated Subtract
    		if (src[x]<B) { 
    			temp=0; 
    		}else{ 
    			temp=(src[x]-B);
    		}
    		temp*=C;  // Multiply with contrast
    		temp/=256; // shift register 
    		dest[x]=(unsigned char)temp;
    	}
    }
    

    Soweit bin ich gekommen, aber jetzt weiss ich nicht wie ich die 16Bit in die 8Bit verkleinern kann????

    void AdjustContrast(unsigned char *dest, const unsigned short* src, unsigned long len,signed short B,signed short C)
    {
      unsigned __in64 maskB = 0;
      unsigned __in64 maskC = 0;
      len = ((len +3) & ~3); // muss durch 4 teilbar sein für MMX
      for (int i = 0; i < 4; i++) { // Maske erstellen
    	maskB = (maskB << 16) || B;
    	maskC = (maskC << 16) || C;
      }
        __asm
        {
                emms                   // mmx-register vorbereiten
                mov ecx, len           
                mov esi, src
                mov edi, dest
                shr ecx, 3             // durch 8 teilen, wir bearbeiten qwords
    	    movq mm2,maskB         //Laden der Helligkeitsmaske (4*16bit)
    	    movq mm3,maskC         //Laden des Kontrastes (4x16bit)
            l0:
                movq mm0, [edi]        // dest[ecx*4] in mmx-register 0
                movq mm1, [esi]        // src[ecx*4] in mmx-register 1
                psubusw mm0, mm1       // packed add of unsigned saturated words, summe in mm0
    //Hier hab ich ein Problem!
                movq [edi], mm0        // summe in dest schreiben
                add edi, 8             // nächstes qword aus dest
                add esi, 8             // nächstes qword aus src
                dec ecx                // zähler verringern
                jnz l0                 // weiter kopieren, wenn werte übrig
                emms                   // mmx-register als unbenutzt markieren
        }
    
    }
    

    Wenn mir jemand weiterhelfen könnte würde ich mich riesig freuen.

    Lg,
    Matthias



  • also ich kann dir http://www.tommesani.com/MMXPrimer.html empfehlen. da findet man unter instruction set -> conversion -> PACKSSDW was vermutlich die instruction ist die du suchst:)


  • Mod

    PACKUSWB dürfte eher zutreffen.
    Allerdings würde ich empfehlen zunächst Standardoptimierungsmittel einzusetzen, der gezeigte C-Code ist ja bei Weitem noch nicht optimal.
    Wieso sind B und C eigentlich vorzeichenbehaftet? Zumindest für B dürfte das nicht beabsichtigt sein.

    void AdjustContrast(unsigned char *dest, const unsigned short* src, unsigned len,unsigned short B,unsigned short C)
    {
        dest += len;
        src += len;
        if ( unsigned len2 = -( len - len % 8 ) )
            do
            {
                dest[(len2+0)]=src[(len2+0)]-B<0?0:(src[(len2+0)]-B)*C/256;
                dest[(len2+1)]=src[(len2+1)]-B<0?0:(src[(len2+1)]-B)*C/256;
                dest[(len2+2)]=src[(len2+2)]-B<0?0:(src[(len2+2)]-B)*C/256;
                dest[(len2+3)]=src[(len2+3)]-B<0?0:(src[(len2+3)]-B)*C/256;
                dest[(len2+4)]=src[(len2+4)]-B<0?0:(src[(len2+4)]-B)*C/256;
                dest[(len2+5)]=src[(len2+5)]-B<0?0:(src[(len2+5)]-B)*C/256;
                dest[(len2+6)]=src[(len2+6)]-B<0?0:(src[(len2+6)]-B)*C/256;
                dest[(len2+7)]=src[(len2+7)]-B<0?0:(src[(len2+7)]-B)*C/256;
            } while ( len2+=8);
        len = -( len % 8 );
        for(;len;len++)
            dest[len]=src[len]-B<0?0:(src[len]-B)*C;
    }
    

    Ist bei mir ungefähr 80% schneller als der ursprüngliche Code. Natürlich lässt sich dann noch etwas mit MMX/SSE2 herausholen, hiermit hat man dann schon einen guten Ausgangspunkt.



  • Hallo,
    danke für die Tipps. Stimmt, PACKUSWB sollte das richtige sein, mir ist noch nicht so ganz klar, wie ich welchen Index erhöhen muß.
    Die Helligkeit (B) ist signed, damit man den Background/Bildhelligkeit sowohl in die eine als auch in die andere Richtung (Dunkler bzw Heller) machen kann. C muß natürlich unsigned sein.
    Ich werde mal probieren, ob "optimierter" C-Code ausreichend schnell ist.

    Lg und vielen Dank,
    Matthias


  • Mod

    PACKSSWB ist das Richtige, weil wir nur arithmetisches Rechtsschift haben.

    void AdjustContrast(unsigned char *dest, const unsigned short* src, unsigned len,unsigned short B,unsigned short C)
    {
        __asm
        {
            movzx   eax, word ptr B
    //        xor     edx, edx
    //        test    eax, eax              // bei vorzeichenbehaftetem B
    //        cmovs   eax, edx
            pinsrw  mm0, eax, 0
            pshufw  mm0, mm0, 0
            movzx   eax, word ptr C
            pinsrw  mm1, eax, 0
            pshufw  mm1, mm1, 0
            mov     eax, len
            add     eax, 7
            and     eax, -8
            mov     ecx, src
            mov     edx, dest
            lea     ecx, [ ecx + eax * 2 ]
            lea     edx, [ edx + eax ]
            neg     eax
    loop_:
            movq    mm2, [ ecx + eax * 2 ]
            movq    mm3, [ ecx + eax * 2 + 8 ]
    
            psubusw mm2, mm0
            psubusw mm3, mm0
    
            pmullw  mm2, mm1
            pmullw  mm3, mm1
    
            psraw   mm2, 8
            psraw   mm3, 8
    
            packsswb mm2, mm3
    
            movq    [ edx + eax ], mm2
    
            add     eax, 8
    
            jnz     loop_
    
            emms
        }
    }
    

    kann man noch durch loop-unrolling und scheduling verbessern.

    Funktion habe ich nicht getest, sollte aber ungefähr stimmen 🙂

    Edit: die vorzeichenbehafte Version ist Blödsinn, muss man anders handhaben, am Besten über einen eigenen Codepfad für den negativen Fall (indem wir einfach unsaturiert subtrahieren). Ich bin immer noch nicht ganz überzeugt, dass es vorzeichenbehaftet und short sein soll.



  • Hallo,
    vielen DANK!!!!!! 🙂
    Ich werde das heute Abend gleich mal ausprobieren und melde mich dann wieder.

    1000 Dank,
    Matthias



  • Hallo mgarza,

    ich würde gerne einen Tipp geben: behalte deine alte Funktion - man kann sie als Normalsterblicher verstehen und debuggen. Wie hast du übrigens festgestellt, dass sie zu langsam ist? Ich würde sie nicht optimieren wollen. Man kann sie vielleicht noch ein wenig umschreiben, etwa so:

    void AdjustContrast(unsigned char *dest, const unsigned short *src, unsigned long len, signed short B, signed short C)
    {
        unsigned long i;
        unsigned short temp;
    
        for (i = 0; i < len; i++)
        {
            if (src[i] < B)
            {
                dest[i] = 0;
            }
            else
            {
                temp = (src[i] - B);
                temp *= C;  // Multiply with contrast
                temp /= 256;
                dest[i] = (unsigned char)temp;
            }
        }
    
        return;
    }
    


  • Hallo Camper,
    ich habe gerade den MMX code ausprobiert, bekomme aber immer einige Fehlermeldungen, daß mm0 und mm1 reservierte Wörter sind:
    Es betrifft folgende Zeilen:
    pinsrw mm0, eax, 0
    pshufw mm0, mm0, 0
    pinsrw mm1, eax, 0
    pshufw mm1, mm1, 0
    Ich hab bislang noch keine Lösung dafür gefunden, muß aber auch zugeben, das meine ASM Kenntnisse sehr bescheiden sind.

    Hallo abc.w,
    ja, ich will die Software auch auf kleinen langsamen PCs laufen lassen und die Loop muß bei jedem Bild einige Millionen mal durchlaufen werden. Aber ich werde den Code aufheben.

    Lg,
    Matthias


  • Mod

    Welchen Compiler verwendest du?
    Ich bin immer noch etwas verwirrt hinsichtlich der Konstanten B und C.
    Naiv würde ich erwarten, dass der Faktor C auch Bruchteile darstellen kann (etwa:B=0,C=1 bildet die Farben in sich selbst ab; B=0,C=.5 halbiert den Kontrast etc.). Wegen das Wertebereichs von unsigned short könnte man dann C ausreichend genau mit einer 32bit-Festkommazahl beschreiben mit dem Komma nach dem 16. bit.
    Man könnte auch negative C in Betracht ziehen, um Helligkeiten zu invertieren.
    Ebenso ist der sinnvoll zulässige Wertebereich für B zwischen -USHORT_MAX...USHORT_MAX - dafür ist ein int notwendig.
    Eigenartig finde ich auch die Behandlung von Überläufen der Multiplikation.
    Führt also die vorgestellte Funktion das, was sie tun soll, aus (wenn auch zu langsam) oder ist sie selbst kaum getestet? Die Optimierung einer fehlerhaften Funktion ist schließlich wertlos. Ist der Wertebereich möglicherweise weiter eingeschränkt, als die Datentypen vermuten lassen?



  • camper schrieb:

    Welchen Compiler verwendest du?

    Visual C++

    camper schrieb:

    Ich bin immer noch etwas verwirrt hinsichtlich der Konstanten B und C. Naiv würde ich erwarten, dass der Faktor C auch Bruchteile darstellen kann (etwa:B=0,C=1 bildet die Farben in sich selbst ab; B=0,C=.5 halbiert den Kontrast etc.). Wegen das Wertebereichs von unsigned short könnte man dann C ausreichend genau mit einer 32bit-Festkommazahl beschreiben mit dem Komma nach dem 16. bit.

    Ja, das wäre natürlich das beste, aber ich habe die Befürchtung, dass könnte dann alles etwas lagsam werden.

    camper schrieb:

    Man könnte auch negative C in Betracht ziehen, um Helligkeiten zu invertieren.
    Ebenso ist der sinnvoll zulässige Wertebereich für B zwischen -USHORT_MAX...USHORT_MAX - dafür ist ein int notwendig.

    Stimmt, das habe ich übersehen. Die software soll für astronomische Kameras verwendet werden, die meist über 16bit verfügen, habe aber bislang alle Test nur mit meiner 8bit Kamera gemacht.

    camper schrieb:

    Eigenartig finde ich auch die Behandlung von Überläufen der Multiplikation.
    Führt also die vorgestellte Funktion das, was sie tun soll, aus (wenn auch zu langsam) oder ist sie selbst kaum getestet? Die Optimierung einer fehlerhaften Funktion ist schließlich wertlos. Ist der Wertebereich möglicherweise weiter eingeschränkt, als die Datentypen vermuten lassen?

    Ja, das Abfangen vom Überlauf habe ich gestern Abend, in meinem normalen C code hinzugefügt. Gebe zu, ich habe die Funktion noch nicht ausreichend getested. Hatte heute mal bei MMX nach Multiply with saturation gesucht und bin leider nicht fündig geworden.

    Lg,
    Matthias



  • Hallo,

    habe ein wenig gerechnet: 25 Bilder pro Sekunde ergibt 40 ms pro Bild.
    Angenommen, diese gute Funktion muss einmal pro Bild aufgerufen werden und die Funktion führt 1e6 Berechnungen durch - ergibt 40 ns pro Berechnung. Die Funktion muss innerhalb von 40 ms fertig werden, ich glaube, es geht nicht...


  • Mod

    abc.w schrieb:

    Hallo,

    habe ein wenig gerechnet: 25 Bilder pro Sekunde ergibt 40 ms pro Bild.
    Angenommen, diese gute Funktion muss einmal pro Bild aufgerufen werden und die Funktion führt 1e6 Berechnungen durch - ergibt 40 ns pro Berechnung. Die Funktion muss innerhalb von 40 ms fertig werden, ich glaube, es geht nicht...

    Nicht zwingend. Erstens entsprechen 40ns erst einmal nur 25MHz - dann hättest du immer noch 8 Takte Zeit bei einem 200er-Pentium je Element, was ggf. ausreicht. Allerdings gibt es eine weitaus bessere Möglichkeit als die Verwendung von MMX. Da du nur 65536 mögliche Ausgangswerte hast, könntest du genauso gut eine LUT verwenden - da sich B und C in der Regel wohl nicht ändern werden, brauchst du sie nicht jedesmal neu berechnen. Das wiederum erlaubt dir den Luxus einen ggf. etwas langsameren, aber dafür genaueren Algorithmus zur Berechnung zu verwenden. Zudem wirst du bei einer Reduzierung auf 8bit Ausgangssignal wahrscheinlich nicht einmal alle 16bit für das Lookup benötigen, bei einer Videoausgabe wird man minimale Ungenauigkeiten wohl ignorieren können, benutzt du z.B. nur die höherwertigen 12bit des Ausgangssignals, braucht die LUT nur 4KB - selbst ein armer Uralt Pentium mit 8KB L1-Cache sollte hiermit keine Schwierigkeiten haben.

    Plötzlich ist das dann kein Assemblerproblem mehr... deinen Compiler solltest du allerdings durch eine neuere Version ersetzen - mit 6.0 ist der Fun- und Performancefaktor einfach tief im Keller.

    Welche Hardware ist denn zur Berechnung gegeben?



  • Hallo,
    bei dieser Applikation (Nachführen von Teleskopen auf Sterne mit einer Astrokamera) sind keine 25fps notwendig, die Bildrate liegt eher im Bereich von 1fps, aber es wäre schön, wenn der Benutzer dieser Software die Berechnung nicht merkt (max 0.1sec), jedoch verwenden viele Leute langsame Rechner, die auch mal kaputt gehen dürfen. Ich selber verwende einen 500MHz PC für meine Astroaufnahmen.

    Was ist eigentlich eine LUT?
    Welchen Compiler sollte man verwenden?

    Lg,
    Matthias

    Ps. Am Wochenede habe ich evtl. kein Internet.


  • Mod

    LUT = look-up table

    enum { table_bits = 12, shift_bits = sizeof( unsigned short ) * CHAR_BIT - table_bits; }; // 12 signifikante Stelle resultiert in
    typedef unsigned char lut_t[ 1 << table_bits ]; // 4KB look-up table
    
    void GenerateLUT(lut_t* table, int B, float C)
    // keine Umstände, Geschwindigkeit ist hier ohnehin sekundär
    // float hat 23 signifikante Stellen, was bei 16 bit Ausgangswerten genügt
    // so müssen wir uns auch nicht um Überläufe bei Zwischenergebnissen sorgen
    {
        for ( unsigned i = 0; i < ( 1 << table_bits ); ++i )
        {
            float v = ( ( i << shift_bits ) - B ) * C;
            (*table)[ i ] = v <= UCHAR_MAX ? v < 0 ? 0 : (unsigned char)v : UCHAR_MAX;
        }
    }
    
    void AdjustContrast(unsigned char *dest, const unsigned short *src, unsigned long len, lut_t* table)
    {
        for ( unsigned long i = 0; i < len; ++i )
            dest[ i ] = (*table)[ src[ i >> shift_bits ] ];
    }
    

    Irgendein neuerer Compiler ist zu empfehlen.
    z.B. Visual C++ Express 9.0 (VS2008)
    oder etwa mingw-gcc (am besten ein 4er build, z.B. gcc-4.2.1)



  • Genial! - Das ist super, damit kann man ja auch generell das Histogramm verändert (e.g. Logaritmisch,exponential,...) und die Table erstellt man einmal und verwendet anschließend bei jedem Bild. Das muß ich heute Abed umbedingt ausprobieren.

    Kann man auch 2^16 Werte verwenden, oder gibt es einen Grund, warum man die Menge auf 4KB statt 64KB reduzieren sollte?

    Danke,
    Matthias


  • Mod

    mgarza schrieb:

    Kann man auch 2^16 Werte verwenden, oder gibt es einen Grund, warum man die Menge auf 4KB statt 64KB reduzieren sollte?

    Selbstverständlich kann man das tun, einfach durch Änderung der einen Konstante. LUTs sollten möglichst klein sein, um in den Prozessor-Cache zu passen (da die Zugriffsreihenfolge keinem bestimmten Muster folgt, kann man nicht sinnvoll prefetchen). Zwingend erforderlich ist es nicht und letztlich eine Frage der Hardwarevoraussetzungen.



  • Ich habe grade eine Weile gebraucht, bis ich in der ganzen Schieberei durchblicke.
    Funktion GenerateLUT - ok, ist zum Debuggen für Unsterbliche.
    Wie ist das mit der Schleife in AdjustContrast(), kann das sein, dass da ein kleiner Tippfehler eingeschlichen ist, nämlich, statt:

    dest[ i ] = (*table)[ src[ i >> shift_bits ] ];
    

    das hier gemeint ist?

    dest[ i ] = (*table)[ src[ i ] >> shift_bits ];
    

  • Mod

    Richtig, Tippfehler. Wäre beim Debuggen wahrscheinlich schnell aufgefallen durch segfault. Man kann die Schieberei natürlich auch durch Multiplikation und Division darstellen, was evtl. lesbarer ist. Ich schreibe für gewöhnlich kein C, das scheint ein bisschen durch.

    enum
    {
        table_bits = 12,
        src_values = USHORT_MAX + 1,
        scale = src_values >> table_bits,
        table_elems = src_values / scale
    };
    typedef unsigned char lut_t[ table_elems ];
    
    void GenerateLUT(lut_t* table, int B, float C)
    {
        for ( unsigned i = 0; i < src_values; i += scale )
        {
            float v = ( i - B ) * C;
            (*table)[ i ] = v <= UCHAR_MAX ? v < 0 ? 0 : (unsigned char)v : UCHAR_MAX;
        }
    }
    
    void AdjustContrast(unsigned char *dest, const unsigned short *src, unsigned long len, const lut_t* table)
    {
        for ( unsigned long i = 0; i < len; ++i )
            dest[ i ] = (*table)[ src[ i ] / scale ] ];
    }
    

    In C++ bietet sich natürlich ein Funktor an.



  • Hallo,
    ich hab jetzt endlich die Routine implementiert. Und alles läuft super. 🙂

    VIELEN DANK für die Hilfe,
    Matthias



  • mgarza schrieb:

    Hallo Camper,
    ich habe gerade den MMX code ausprobiert, bekomme aber immer einige Fehlermeldungen, daß mm0 und mm1 reservierte Wörter sind:
    Es betrifft folgende Zeilen:
    pinsrw mm0, eax, 0
    pshufw mm0, mm0, 0
    pinsrw mm1, eax, 0
    pshufw mm1, mm1, 0
    Ich hab bislang noch keine Lösung dafür gefunden, muß aber auch zugeben, das meine ASM Kenntnisse sehr bescheiden sind.

    Das Problem, dass "pinsrw" und "pshufw" von VC6 nicht übersetzt werden, liegt daran, dass diese Befehle erst mit der SSE-Spezifikation eingeführt wurden. In der Version 6 wird standardmäßig aber nur maximal MMX unterstützt. Um die Anweisungen kompilieren zu können, mußt Du das "Visual C++ 6.0 Processor Pack" von MS einspielen. Aber Vorsicht: Der Code läuft dann auch nur auf Prozessoren, die SSE unterstützen, ansonsten gibt's 'ne Exception. 😉

    tl123


Anmelden zum Antworten