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.


  • Mod

    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 😃


  • Mod

    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);
    }
    

Anmelden zum Antworten