Optimierung ist langsamer...
-
Naja sowas ähnliches dachte ich mir auch schon... daß der Call wegen Multitasking möglicherweise geschickter für das OS ist, aus irgendwelchen Gründen. Aber komisch, daß es jetzt noch schneller geht, wo ich ja nun Jumps verwende...
Naja ich kann's halt schlecht unter was anderem testen. Meine Meßmethode dürfte nicht falsch sein, die liefert halt die Millisekunden der gesamten Schleife. Und die führ ich jedesmal mehrmals aus, und natürlich ist die Zeit nicht immer exakt gleich, aber nach 5 Durchläufen kann man eigentlich schon eine Tendenz erkennen, und die ist halt Original 922, Optimierung1 1015, und Optimierung2 890.
An einer anderen Stelle habe ich die gleiche Funktion drin (es handelt sich um die abs()-Funktion), hier war es komischerweise egal, ob ich das über den Call mache oder direkt inline.
-
vista meine eher, dass das Programm langsammer sein kann, weil zwischendurch ein anderes Programm mal die CPU verwenden durfte.
Wenn du das wirklich messen willst brauchst du wohl einen Profiler, ich glaube von AMD gibts einen kostenlosen, hab das aber nie verwendet
-
Naja okay so kann man's auch sehen, aber ich glaube ehrlich gesagt kaum, daß das so einen Einfluß drauf hat. Ich hab das Programm nach jedem Kompilieren mindestens 5 - 10x durchlaufen lassen, und die Zeitergebnisse sind schon immer sehr konsistent, z.B. bei der Originalversion kommt sehr oft 922 ms raus, aber manchmal auch 906 oder 949. Bei den anderen war es ähnlich, also es hat sich immer deutlich ein gewisser Bereich herauskristallisiert, der nach etlichem Testen schon eine klare Richtung angegeben hat.
Zwischenzeitlich hab ich ja auch oft wieder das neue auskommentiert und das alte wieder mit reingenommen usw, und jedesmal waren die Zeiten wieder entsprechend. Also Multitasking hin oder her, auf die genauen ms kommt es mir nicht an, aber der Unterschied ist wirklich deutlich zu erkennen.
EDIT: Also das gesamte Programm ist wirklich nur diese eine Schleife und somit nach rund einer Sekunde beendet. Dementsprechend könnt ihr euch ja sicher vorstellen, wie ich vor dem Rechner sitze und dauernd "Pfeil nach oben" und "Enter" in der Console drücke
ich denke schon daß das aussagekräftig ist.
-
Die Messung der Durchschnittsdauer ist für so kurze Zeiten nicht optimal. Sinnvoller ist es, die minimale Laufzeit zu bestimmen. Durch genauere Messung (rdtsc) lassen sich so externe Störeffekte normalerweise vollständig eliminieren, das Ergebnis ist dann immer reproduzierbar.
Etwa für einen 1GHz Prozessor (ohne Last) kann man ohne Weiteres annehmen, dass ein Prozess oft wenigstens 10.000 Takte am Stück ausführen kann. Das gezeigte Codestück benötigt mit Sicherheit weniger als 100 Takte, somit ist eine Schleife mit dem Faktor 100 angemessen (und durch diese Schleife eliminieren wir die Probleme wegen der fehlenden Serialisierung von rdtsc):typedef unsigned __int64 u64; u64 rdtsc() { __asm rdtsc } u64 best_time() { u64 best = -1; for ( int i = 101; --i; ) { u64 t = rdtsc(); for ( int j = 101; --j; ) { // hier der zu messende Code // ... } t = rdtsc() - t; if ( t < best ) { best = t; i = 101; } } return best; }
Wir wiederholen die Messung also solange, bis die Bestzeit 100 mal nicht unterboten wurde. Damit haben wir ein Ergebnis in Prozessortakten, was leicht umgerechnet werden kann. In Mehrprozessor- und Multicoresystemen ist es zweckmäßig, den Prozess an eine logische CPU zu binden, da die Timestampcounter der einzelnen CPUs nicht synchron laufen. Das geht zum Beispiel kurz und schmerzlos so:
#include <windows.h> struct SetCPUAffinity { SetCPUAffinity() { SetProcessAffinityMask( GetCurrentProcess(), 1 ); Sleep( 0 ); } } affinity;
Soweit zur Messsung.
Dass der gezeigte Code in der Urfassung möglicherweise schneller ist, ist nicht völlig abwegig. Tatsächlich stellt der zusätzliche Code keine echte Last dar, da die Laufzeit vom idiv dominiert wird, können die folgenden Befehle ohne Probleme parallel ausgeführt werden (das mov eax, brauch wegen Store-Load-Forwarding nicht auf das push zu warten, cdq kann sofort beginnen, wenn idiv fertig ist). Auch das ret wird im Allgemeinen nicht in Erscheinung treten (Für diese Optimierung existiert ein extra Speicher). Auf alle Fälle dürfte das Verhalten auch vom eingesetzten Prozessor abhängen - den du nicht näher spezifiziert hast. Poste doch am besten mal vollständig compilierbaren Code zum Nachvollziehen. In real existierendem Code mag sich das Verhalten durchaus von einer einfachen Messschleife wie hier unterscheiden.
-
Okay, also hier mal der Originalcode.
Ich möchte vielleicht noch dazusagen, daß es dafür gedacht ist, für das Zeichnen eines WAV-Files die Audio-Daten vorzubuffern. Das ist noch nicht die Endversion, deswegen wird hier z.B. nicht auf den Wave-Header eingegangen, sondern bestimmte Werte werden von vorneherein gesetzt (z.B. bytesPerSample oder sowas). Somit könnt ihr jede beliebige Datei als Parameter angeben. Ich habe es immer mit einem 80-MByte-File ausprobiert. Für längere Laufzeiten müßtet ihr also einfach größere Files benutzen#include <time.h> #include <iostream> #include <deque> int sampleSize = 2048; int cycles = 1; int bytesPerSample = 2; int numChannels = 1; int average; int numBytes = 0; std::deque<int> averageList; inline int waveToAverage(char* data, int bufferSize, int cycle) { int bytesPerFrame = bytesPerSample * numChannels; long int sum = 0; int sample; int idx = (int)((float)(bufferSize / cycles) * cycle); int temp; /* bufferSize /= bytesPerFrame; bufferSize /= cycles; */ asm { mov eax, bufferSize xor edx, edx idiv bytesPerFrame xor edx, edx idiv cycles mov bufferSize, eax } //for(i=0; i<bufferSize; i+=bytesPerSample) //{ // sample = 0;/* asm { xor ebx, ebx start_i: xor eax, eax mov sample, eax } //for(j=0; j<numChannels; j++) //{ // temp = 0; asm { push ebx xor ebx, ebx start_j: xor eax, eax mov temp, eax } //for(k=0; k<bytesPerSample; k++) // temp += (int)data[idx + k] << k*8; asm { push ebx xor ebx, ebx start_k: mov ecx, idx add ecx, ebx push ebx mov ebx, [ebp+8] movsx esi, [ecx+ebx] pop ebx mov ecx, ebx shl ecx, 3 shl esi, cl add temp, esi inc ebx cmp ebx, bytesPerSample jb start_k pop ebx } /* for(k=0; k<bytesPerSample; k++) asm { mov ecx, idx add ecx, k mov ebx, [ebp+8] movsx esi, [ecx+ebx] mov ecx, k shl ecx, 3 shl esi, cl add temp, esi }*/ //if (bytesPerSample == 1) // 8 bit unsigned // temp = 128 - abs(temp); asm { test bytesPerSample, 1 jz nothingtodo mov eax, temp cdq xor eax, edx sub eax, edx mov ecx, 0x80 sub ecx, eax mov temp, ecx nothingtodo: } //idx += bytesPerSample; asm { mov eax, idx add eax, bytesPerSample mov idx, eax } //sample += temp; asm { mov eax, sample add eax, temp mov sample, eax } //} // end of j-loop asm { inc ebx cmp ebx, numChannels jb start_j pop ebx } //sum += abs(sample / numChannels); asm { mov eax, sample cdq idiv numChannels jmp absolute retabsolute: add sum, eax jmp outhere absolute: cdq xor eax, edx sub eax, edx jmp retabsolute outhere: } /* //the old version, without jumps, but longer execution time %-( asm { mov eax, sample cdq idiv numChannels cdq xor eax, edx sub eax, edx add sum, eax } */ //} // end of i-loop asm { add ebx, bytesPerSample cmp ebx, bufferSize jb start_i } return (int)(sum / bufferSize); } void bufferAudioData(FILE* inStream) { int bufferSize = sampleSize * bytesPerSample; char bytes[bufferSize]; averageList.clear(); int cnt = 0; while(true) { numBytes = fread(bytes, 1, bufferSize, inStream); if (numBytes <= 0) break; average = waveToAverage(bytes, bufferSize, 0); /* if (cnt % 50 == 0) { printf("%i\n", average); } */ averageList.push_back(average); //cnt++; } return; } int main(int argc, char *argv[]) { clock_t start, end; printf("\n\nfile: %s\n\n", argv[1]); start = clock(); FILE* inFile = fopen(argv[1], "rb"); if (inFile == NULL) { printf("file not found!"); return 0; } sampleSize = 4096; bufferAudioData(inFile); fclose(inFile); end = clock(); printf("\ntime: %i\n", end); return 0; }
Die kürzere aber dafür länger aussehende Version des Codeschnipsels, um das es geht, ist auskommentiert. Um es zu testen, müßtet ihr also einmal von Zeile 172 an bis zum Label "outhere" auskommentieren und das darunterliegende Code-Stück verwenden.
Ich hab es auf einem Pentium 4 probiert, unter Windows XP. Ich sollte vielleicht noch dazusagen, daß der erste Start des Programms nach dem Kompilieren immer ca. 2,5 Sekunden dauert, daher unbedingt mehrmals ausführen. Beim zweiten Durchlauf ist es in der Regel schon bei den ca. 828 Millisekunden (mittlerweile wurden noch viele andere Dinge durch ASM ersetzt, daher ist es nun noch etwas kürzer).
-
WICHTIG BEVOR IHR DAS OBERE COMPILIERT
Ich hab es jetzt zum Spaß nochmal probiert, auszutauschen - mittlerweile dauert die Version, die im Originalposting die langsamste war, nur noch 700 ms im Vergleich zu den oben erwähnten 828 ms.Es ist komisch
Tja jetzt werd ich natürlich doch die optimierte Fassung nehmen. Komisch, daß es vorhin genau umgedreht war...In dem Fall werdet ihr mir wohl auch nicht mehr großartig weiterhelfen können... es sei denn, ihr wandelt alles ASM-mäßige wieder in C++ um, bis auf die betreffende Stelle
-
Grausam.
Ich wage zu behaupten, dass du an der falschen Stelle optimierst. Gibts die Funktion auch noch in C++, so dass man etwas verstehen kann?
-
Also im Prinzip steht über jedem ASM-Block das entsprechende C-Äquivalent
Ich weiß, es mag übertrieben scheinen, daß hier alles, sogar die For-Schleifen optimiert wurden, und ich hab i.d.R. auch nicht den Drang, jede nano-Sekunde rauszuholen - in diesem Fall aber wollte ich die maximale Geschwindigkeit haben, auch weil das hier eins meiner ersten Optimierungs-Experimente ist.
Ursprünglich war das, wie gesagt, reines C, und ich hab nach und nach die Zeilen auskommentiert, durch ASM-Blöcke ersetzt, und immer wieder getestet, ob noch das gleiche ausgeführt wird und ob es schneller wurde. Nun ist es insgesamt ca. 35% schneller als die reine C-Version, was ich ja auch sehr begrüße.
Jedenfalls trat das eigentliche Problem, also der Grund für diesen Thread, schon sehr früh auf, als der Rest noch C war. Nun, wo eigentlich die komplette Funktion in ASM geschrieben ist (die einzelnen ASM-Blöcke sind momentan nur noch deswegen drin, damit ich noch die ursprüngliche C-Struktur erkennen kann), tritt das Problem komischerweise nicht mehr auf, sondern wenn ich jetzt meine optimierte Version benutze, ist es in der Tat schneller - also so wie ursprünglich erwartet.
Ob mein Optimierungswahn im Gesamten also gut oder schlecht ist, ist jetzt im Moment komplett unwichtig, es geht mir eher ums Experimentieren, und es ist halt seltsam, daß im Fast-Urzustand die eine Möglichkeit langsamer war, sie im End-Zustand nun aber wie erwartet nochmal einen deutlichen Geschwindigkeitsschub liefert
und falls irgendjemand wissen sollte, wieso das so ist, hoffe ich daß dieser es mir erklären wird
-
Um es vielleicht nochmal kurz auszudrücken, damit auch wirklich klar ist, was gemeint ist:
Wenn ich den Block von Zeile 172 an mit dem von Zeile 193 an vertausche, gewinne ich in meiner Schleife auf einmal 100 ms Geschwindigkeit. Als der Code noch fast reines C war, und nur der besagte Block in ASM geschrieben war, war es eher andersrum - der längere Block war wider Erwarten der schnellere.
-
Hier nochmal der ursprüngliche C-Code (ich hoffe mal, ich hab es korrekt zurückgebastelt, hab's jetzt nicht nochmal kompiliert):
inline int waveToAverage(char* data, int bufferSize, int cycle) { int bytesPerFrame = bytesPerSample * numChannels; long int sum = 0; int sample; int idx = (int)((float)(bufferSize / cycles) * cycle); int temp; bufferSize /= bytesPerFrame; bufferSize /= cycles; for(i=0; i<bufferSize; i+=bytesPerSample) { sample = 0; for(j=0; j<numChannels; j++) { temp = 0; for(k=0; k<bytesPerSample; k++) temp += (int)data[idx + k] << k*8; if (bytesPerSample == 1) // 8 bit unsigned temp = 128 - abs(temp); idx += bytesPerSample; sample += temp; } sum += abs(sample / numChannels); } return (int)(sum / bufferSize); }