Fehler bei gcc / Code::Blocks?



  • Guten Abend,

    ich wende mich heute Abend an euch, da ich mir ein seltsames Verhalten nicht erklären kann. Hier einmal der betreffende Code:

    #include <iostream>
    #include <ctime>
    
    using namespace std;
    
    int main()
    {
    // Variabeln für die Durchläufe
    // Und den Test der durchschnittlichen
    // Laufzeit einzelner Operation auf die
    // jeweiligen Standard Datentypen
    // Variable zum einstellen der Durchläufe
        int avg_run = 50;
    // Variable zum einstellen der Operationen
        long test_run = 10000000;
    
        short t_elapse = 0;
    
        short st_short = 10;
        short nd_short = 10;
    
        int st_int = 10;
        int nd_int = 10;
    
        long st_long = 10;
        long nd_long = 10;
    
        float st_float = 10.0;
        float nd_float = 10.0;
    
        double st_double = 10.0;
        double nd_double = 10.0;
    
        long stop = 0;
        clock_t start;
    
        int avg_short_add_sub = 0, avg_short_mul_div = 0, avg_int_add_sub = 0, avg_int_mul_div = 0;
        int avg_long_add_sub = 0, avg_long_mul_div = 0;
        int avg_float_add_sub = 0, avg_float_mul_div = 0, avg_double_add_sub = 0, avg_double_mul_div = 0;
    
    // Probe schleife (wiederholt sich für jedenen Datentyp)
        while(t_elapse < avg_run)
        {
        // Startzeit speichern
            start = clock();
            // Operationen ausführen
            while(stop < test_run)
            {
                nd_short += st_short;
                nd_short -= st_short;
                ++stop;
            }
            // Zeit Speichern
            avg_short_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_short *= st_short;
                nd_short /= st_short;
                ++stop;
            }
            avg_short_mul_div = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_int += st_int;
                nd_int -= st_int;
                ++stop;
            }
            avg_int_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_int *= st_int;
                nd_int /= st_int;
                ++stop;
            }
            avg_int_mul_div = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_long += st_long;
                nd_long -= st_long;
                ++stop;
            }
            avg_long_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_long *= st_long;
                nd_long /= st_long;
                ++stop;
            }
            avg_long_mul_div = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_float += st_float;
                nd_float -= st_float;
                ++stop;
            }
            avg_float_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_float *= st_float;
                nd_float /= st_float;
                ++stop;
            }
            avg_float_mul_div = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_double += st_double;
                nd_double -= st_double;
                ++stop;
            }
            avg_double_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
        while(t_elapse < avg_run)
        {
            start = clock();
            while(stop < test_run)
            {
                nd_double *= st_double;
                nd_double /= st_double;
                ++stop;
            }
            avg_double_mul_div = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        // resultate Ausgeben
        cout << "Durchschnittliche Zeit für Short Addition/Subtraction: \t\t";
        cout << (avg_short_add_sub / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Short Multipikation/Division: \t";
        cout << (avg_short_mul_div / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Int Addition/Subtraction: \t\t";
        cout << (avg_int_add_sub / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Int Multipikation/Division: \t\t";
        cout << (avg_int_mul_div / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Long Addition/Subtraction: \t\t";
        cout << (avg_long_add_sub / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Long Multipikation/Division: \t";
        cout << (avg_long_mul_div / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Float Addition/Subtraction: \t\t";
        cout << (avg_float_add_sub / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Float Multipikation/Division: \t";
        cout << (avg_float_mul_div / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Double Addition/Subtraction: \t";
        cout << (avg_double_add_sub / avg_run) << endl;
        cout << "Durchschnittliche Zeit für Double Multipikation/Division: \t";
        cout << (avg_double_mul_div / avg_run) << endl;
        return 0;
    }
    

    Wenn ich in der IDE nun auf Debug stelle,
    erzeugt der Code folgende Ausgabe:

    • Durchschnittliche Zeit für Short Addition/Subtraction: 1109
    • Durchschnittliche Zeit für Short Multipikation/Division: 2586
    • Durchschnittliche Zeit für Int Addition/Subtraction: 1000
    • Durchschnittliche Zeit für Int Multipikation/Division: 2590
    • Durchschnittliche Zeit für Long Addition/Subtraction: 1000
    • Durchschnittliche Zeit für Long Multipikation/Division: 7319
    • Durchschnittliche Zeit für Float Addition/Subtraction: 1501
    • Durchschnittliche Zeit für Float Multipikation/Division: 2836
    • Durchschnittliche Zeit für Double Addition/Subtraction: 1501
    • Durchschnittliche Zeit für Double Multipikation/Division: 4174

    Wenn ich jetzt jedoch die IDE auf Release stelle, ist die Ausgabe für alles 0, egal wie hoch ich test_run stelle. Selbst wenn ich für test_run 100000000000000000 eingebe, ist das Programm umgehende fertig 😮 😕

    Weiß eventuell jemand eine Lösung, oder bin ich hier tatsächlich auf einen Fehler gestoßen?



  • Supercomputer schrieb:

    Wenn ich jetzt jedoch die IDE auf Release stelle, ist die Ausgabe für alles 0, egal wie hoch ich test_run stelle.

    Erste Vermutung: Der Compiler erkennt, dass es sich bei deinen Ergebnissen alle um konstante Werte handelt, und inlined den Code so, dass nur noch die konstanten Werte geladen werden:

    call   400910 <clock@plt>
    mov    r15,rax
    call   400910 <clock@plt>
    

    Und Registerzuweisung ist nichts. Da musst du schon auf Cycleebene messen, um überhaupt ein Ergebnis zu erhalten.

    Die Optimierungen im Release-Mode ergeben auch Sinn. Wenn du debuggst, dann musst du eventuell in der Lage sein, direkt nachzuvollziehen, welche Zeile welchen Maschinencode generiert hat. Dann lässt der Compiler die Optimierungen stecken, ist ja nicht wichtig für dich. Im Release-Mode hingegen soll das Teil geshippt werden, da sind Debugging-Features fast komplett uninteressant, und dann möchtest du auch Optimierungen.



  • Um solche Optimierungen (im Besonderen die sog. Konstantenfaltung) auf simple Weise zu vermeiden kann man übrigens tatsächlich mal das selten benötigte Schlüsselwort volatile einsetzen.
    Dieses besagt in etwa: Hey Compiler, auch wenn du zu wissen glaubst welchen Wert diese Variable hat, sie kann auch einen völlig anderen Wert haben. Vermeide also bitte alle Optimierungen
    die auf der Annahme beruhen, dass du ihren Wert kennst.

    Bei deinem Programm sollte es z.B. reichen einmal eine neutrale Rechenoperation mit einer volatile -qualifizierten Variable auf den Variablen zu machen mit denen du die eigentlichen Berechnungen in der Schleife durchführst:

    ...
        volatile int v = 0;
        nd_short += v;
        nd_int += v;
        nd_long += v;
        nd_float += v;
        nd_double += v;
    
    // Probe schleife (wiederholt sich für jedenen Datentyp)
        while(t_elapse < avg_run)
        {
            ...
    

    Jetzt muss der Compiler auch Code erzeugen um die eigentlichen Berechnungen durchzuführen, da er nicht mehr davon ausgehen darf, dass die nd_* -Variablen die gesetzen konstanten Werte haben.
    Tatsächlich wird natürlich dennoch nur 0 addiert, daher werden die gesetzten Werte nicht wirklich verändert.

    Das reicht bei deinem Programm allerdings noch nicht ganz, da sich hier auch noch andere Optimierungen auswirken:

    z.B. keine weitere Verwendung der berechneten nd_* -Variablen im Programm, daher können die Schleifen, die diese Werte verändern, einfach rausopimiert werden.
    Hier hilft am Ende der Schleifen die Variablen einfach nochmal in eine volatile -Variable zu schreiben (oder sie z.B. via cout auszugeben):

    ...
    // Probe schleife (wiederholt sich für jedenen Datentyp)
        while(t_elapse < avg_run)
        {
            ...
        }
        v = nd_short;
        v = nd_int;
        v = nd_long;
        v = nd_float;
        v = nd_double;
        ...
    

    Auch die Tatsache dass sich die in den Schleifen durchgeführten Rechenoperationen gegenseitig aufheben könnte aufgrund von Optimierungen das Ergebnis verfälschen
    (Zumindest bei Integer Addition/Subtraktion. Integer-Multiplikation/Division und inverse Operationen in Fliesskoma-Arithmetik sind nicht in allen Fällen wirklich "invers").
    Hier würde es evtl. Sinn machen Addition/Subtraktion und Multiplikation/Division mit jeweils anderen Werten durchzuführen.

    Bei meinem oberflächlichen Test mit GCC und Clang scheint aber der volatile -Trick auszureichen.

    Gruss,
    Finnegan



  • Finnegan schrieb:

    Um solche Optimierungen (im Besonderen die sog. Konstantenfaltung) auf simple Weise zu vermeiden kann man übrigens tatsächlich mal das selten benötigte Schlüsselwort volatile einsetzen.
    Dieses besagt in etwa: Hey Compiler, auch wenn du zu wissen glaubst welchen Wert diese Variable hat, sie kann auch einen völlig anderen Wert haben. Vermeide also bitte alle Optimierungen
    die auf der Annahme beruhen, dass du ihren Wert kennst.

    Bei meinem oberflächlichen Test mit GCC und Clang scheint aber der volatile -Trick auszureichen.

    Gruss,
    Finnegan

    Vielen Dank für den Tipp, jetzt funktioniert es.
    Ich kannte volatile bisher nur aus C in Zusammenhang mit der Programmierung einer ISR auf einem Mikrocontroller.
    Jetzt läuft das Programm, vielen Dank dafür.

    Was ich mit dem Programm eigentlich untersuchen Möchte, ist der Verbrauch an CPU Zeit für die jeweilige Operation, damit ich beliebig viele Durchläufe testen kann und nicht den Variable überlaufen lasse, ist mir keine andere Lösung eingefallen, als diese Null Operationen in der jeweiligen Schleife.

    Nun gibt es jedoch unerwartete Ergebnisse (liegt es wieder an der Optimierung)?
    Auf einem Q6600 erhalte ich als Durchschnittswerte beim Debug:

    • Durchschnittliche Zeit für Short Addition/Subtraction: 1585
    • Durchschnittliche Zeit für Short Multipikation/Division: 3085
    • Durchschnittliche Zeit für Int Addition/Subtraction: 1501
    • Durchschnittliche Zeit für Int Multipikation/Division: 3086
    • Durchschnittliche Zeit für Long Addition/Subtraction: 1501
    • Durchschnittliche Zeit für Long Multipikation/Division: 7714
    • Durchschnittliche Zeit für Float Addition/Subtraction: 2252
    • Durchschnittliche Zeit für Float Multipikation/Division: 3628
    • Durchschnittliche Zeit für Double Addition/Subtraction: 2252
    • Durchschnittliche Zeit für Double Multipikation/Division: 4837

    Demnach ist die Long Multiplikatiion/Division mit Abstand das langsamste. Nun ist es im Releasemode vollständig anders:

    • Durchschnittliche Zeit für Short Addition/Subtraction: 83
    • Durchschnittliche Zeit für Short Multipikation/Division: 1059
    • Durchschnittliche Zeit für Int Addition/Subtraction: 83
    • Durchschnittliche Zeit für Int Multipikation/Division: 83
    • Durchschnittliche Zeit für Long Addition/Subtraction: 166
    • Durchschnittliche Zeit für Long Multipikation/Division: 166
    • Durchschnittliche Zeit für Float Addition/Subtraction: 750
    • Durchschnittliche Zeit für Float Multipikation/Division: 2085
    • Durchschnittliche Zeit für Double Addition/Subtraction: 771
    • Durchschnittliche Zeit für Double Multipikation/Division: 3336

    Was den erwarteten Werten entspricht, bis auf den Ausreißer bei der Short Multiplikation/Division.

    int main()
    {
    // Variabeln für die Durchläufe
    // Und den Test der durchschnittlichen
    // Laufzeit einzelner Operation auf die
    // jeweiligen Standard Datentypen
        short t_elapse = 0;
        volatile int v = 0;
    

    (...)

    while(t_elapse < avg_run)
        {
        // Startzeit speichern
            start = clock();
            // Operationen ausführen
            while(stop < test_run)
            {
                nd_short += st_short;
                nd_short -= st_short;
                nd_short += v;
                ++stop;
            }
            // Zeit Speichern
            avg_short_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
    

    (...)

    v = nd_double;
        v = nd_float;
        v = nd_int;
        v = nd_long;
        v = nd_short;
        // resultate Ausgeben
    

    Auf einem Sempron 3400+

    Erhalte ich bis auf die erwarteten erhöhten Werte, ähnliche Resultate.

    Ist das so normal und richtig, oder hat sich da noch ein Fehler eingeschlichen?
    Denn genau die Laufzeit will ich ja eigentlich untersuchen 😕



  • Supercomputer schrieb:

    while(t_elapse < avg_run)
        {
        // Startzeit speichern
            start = clock();
            // Operationen ausführen
            while(stop < test_run)
            {
                nd_short += st_short;
                nd_short -= st_short;
                nd_short += v;
                ++stop;
            }
            // Zeit Speichern
            avg_short_add_sub = (clock() - start);
            ++t_elapse;
            stop = 0;
        }
        t_elapse = 0;
    

    Steht die Zeile 10 ( nd_short += v; ) so nur in der short -Schleife oder in allen Schleifen?
    Für den "volatile-Trick" ist es nicht notwendig, und je nachdem was du wirklich messen willst sogar schädlich,
    dass du innerhalb der Schleifen nochmal diese Operation mit v stehen hast:

    Jedes mal wenn ein volatile in dieser Form verwendet wird, muss der Compiler nämlich explizit Code erzeugen,
    mit dem der Wert aus dem Arbeitsspeicher gelesen wird. In diesem Fall kann der Schleifencode z.B. nicht ausschliesslich
    auf CPU-Registern arbeiten. Du wirst also wahrscheinlich hauptsächlich den Speicherzugriff/CPU-Cache messen anstatt
    die Arithmetik-Leistungsfähigkeit der CPU, was du vielleicht tatsächlich wissen möchtest.

    Ich habe den "volatile-Trick" extra so formuliert, dass der Compiler dennoch die Möglichkeit hat die CPU in den Rechenschleifen
    ausschliesslich auf Registern arbeiten zu lassen - die " += v; " auf den Variablen, bevor er in die Schleifen eintritt, reichen dafür
    völlig aus. Ansonsten hätte man auch auf den Umweg über das volatile v verzichten und die nd_ -variablen direkt volatile
    deklarieren können, dann hat man auch jedes mal den Speicherzugriff.

    Trotz allem: Wenn du wirklich präzise messen willst, macht es mehr Sinn den Messcode in Assembler zu schreiben oder genau
    darauf zu achten was für ein Code tatsächlich vom Compiler erzeugt wird. Sonst lässt sich nachher nur schwer feststellen was
    man da tatsächlich misst (CPU? CPU-Cache? Speicher? Pfiffige Optimierungen?).

    Finnegan



  • Vielen Dank für diese erneut sehr gute Hilfe.

    Nein, die += v hatte ich in jeder Schleife stehen.
    Mit deinem Code, wird es in meinen Augen noch etwas seltsamer 😕,
    alle ganzzahligen Operationen bis auf die Multiplikation/Division von Short werden mir nun mit 0 ausgegeben,
    selbst bei einer Erhöhung der Test-Läufe von 10 Millionen auf 1 Milliarden.
    Resultate des Q6600 bei 1 Milliarden Testläufe wie folgt:

    • Durchschnittliche Zeit für Short Addition/Subtraction: 0
    • Durchschnittliche Zeit für Short Multipikation/Division: 98492
    • Durchschnittliche Zeit für Int Addition/Subtraction: 0
    • Durchschnittliche Zeit für Int Multipikation/Division: 0
    • Durchschnittliche Zeit für Long Addition/Subtraction: 0
    • Durchschnittliche Zeit für Long Multipikation/Division: 0
    • Durchschnittliche Zeit für Float Addition/Subtraction: 50054
    • Durchschnittliche Zeit für Float Multipikation/Division: 183505
    • Durchschnittliche Zeit für Double Addition/Subtraction: 50048
    • Durchschnittliche Zeit für Double Multipikation/Division: 308593

    Die Short Multiplikation/Division ist weiterhin deutlich langsamer als die Addition/Subtraktion der Fließkommazahlen.

    Mit Assembler habe ich mich bisher nur am Rande beschäftigt,
    da müsste ich mich dann einmal ordentlich einlesen.
    Nur muss ich dann nicht für jede zu testende CPU einen eigenen Code schreiben,
    oder sind die Unterschiede (Ich nenne sie jetzt mal Dialekte) nicht so gravierend in den verschiedenen CPU Generationen,
    getestet werden sollen CPUs die den x64 Befehlssatz unterstützen.
    Mit Assembler könntest du recht haben, denn direkter kann man meines Wissens nicht auf die CPU zugreifen.
    Gibt es ebenso einen Assembler für die Programmierung von GPUs? Dies sind ja doch schon "etwas" anders aufgebaut.

    Der Zweck dieses Projektes ist,
    zu ermitteln bei welchem Datentyp und ab welcher Datenmenge es sich lohnt, auf die GPU statt der CPU zu setzen.

    Das erwartetes Resultat bei den GPU Tests: Fließkommaoperationen sind deutlich schneller als Ganzzahloperationen.
    (Jedoch schlage ich mich noch mit OpenCL herum und fühle mich, als wenn ich auf der Stelle trete)

    Bitte entschuldigt, falls das jetzt alles etwas durch gewürfelt klingt 😮 😕


Anmelden zum Antworten