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:)
-
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
-
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
-
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...
-
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,
MatthiasPs. Am Wochenede habe ich evtl. kein Internet.
-
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
-
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 ];
-
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