Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise



  • Eine Gefahr besteht in dem Sinne nicht, da diese Ersetzungen nur vorgenommen werden, wenn sie nicht zu durch anderen Code beobachtbaren Änderungen führt. Wenn du std::cout << mal(5,7) schreibst kann da durchaus std::cout << 35 im rauskommen. Konkret kann man das aber nicht sagen, was in Einzelfall passiert müsste man immer nachschauen, wenn es einen so dringend interessiert.



  • @axam sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:

    Wie weitreichend können diese Optimierungen sein?

    So klug wie die Compilerentwickler waren - solange du keinen Unterschied in der Ausgabe siehst.

    Sogar sowas wie die Summe der Zahlen von 1 bis n wird von clang durch die Gauß-Summenformel ersetzt.

    Schau dir das doch in godbolt an, wozu haben wir da schon hingelinkt?

    Hier: https://gcc.godbolt.org/z/YEWPKd
    Deine plus-Funktion: Der Parameter w1 wird (unter Linux x86_64) in rdi und w2 im Register rsi übergeben.
    Also

    int plus(int edi, int esi) {
      int eax = 0;               //  xor     eax, eax
      if (esi nicht negativ) {   //  test    esi, esi [1]
        eax = esi;               //  cmovns  eax, esi [1a]
      }
      eax += edi;                //  add     eax, edi
      return eax;                //  ret
    

    [1] und [1a] gehören zusammen - test ist hier "setze sign-Bit" und das cmov (conditional move) ist ein "if + Zuweisung zusammen": move wenn nicht sign-Bit gesetzt"

    Du siehst also: deine Schleife ist weg.

    Und wenn du z.B. noch -march=skylake als Parameter angibst, ist der Algorithmus anders...

    Und wenn du deine mal-Funktion einfach mal in Godbolt reinkopierst, wirst du da ein "imul", also eine Multiplikation, finden...



  • @axam: Noch zur Info für dich: ein Prozessor verwendet intern dafür keine Schleifen, sondern die Hardware-Schaltungen Addierwerk und Multiplizierer.
    Die Performance ist also unabhängig von den verwendeten Summanden (bzw. Faktoren), sondern nur von der Bitbreite.



  • @wob: Danke daß du mich an godbold erinnerst, hab irgend wie gar nicht daran gedacht ... "Brett vorm Kopf". ("grins")

    Also was ich hier so höre gefällt mir irgend wie gar nicht.
    (Liegt nicht an euch, sondern an den Gegebenheiten).

    Dann werd ich mal weiter überlegen, wie ich daß ganze Realisieren könnte.

    Um mein Dilemer vieleicht etwas nachvollziehbarer zu machen und dann eventuell sogar eine gute Anregung für eine Lösung zu bekommen, hier nun wofür ich es brauchen könnte:

    Meine Überlegungen sind zwar vorerst nur theoretischer Natur ist jedoch an eine Reale Situation angelehnt:
    Ein kleines Kind rechnet nicht, es Zählt:
    1-2-3-4Finger + 1-2-3-4Finger = 1-2-3-4-5-6-7-8Finger
    Ein Erwachsener (jemand der schon Rechnen kann) zählt nicht, sondern hat bereits Ergebnisse auswendig gelernt, die er nun nur noch miteinander verknüpfen muß. Wenn jemand sagt 4+4 weis er sofort das es 8 ist, und bei 44+44 zählt er nicht bis 88 sondern rechnet:
    Stelle1: 4+4=8
    Stelle2: 4+4=8
    Stelle1 Stelle2 = 88

    Nun mache ich mir gedanklich ein Objekt "Kind" und ein Objekt "Erwachsener" und weise ihnen verschiedene "Eigenschaften" zu.
    Bei der "Eigenschaft" "Mathematik/Logik" "erbt" "Erwachsener" zwar von "Kind" die "Fähigkeit" "Zählen()" verfügt aber zusätzlich über die "Fähigkeit" "Rechnen()".
    Stelle ich jetzt beiden Objekten("Erwachsener" & "Kind") die Aufgabe eine Liste mit Rechenaufgaben zu lösen, sollte entsprechend der AlltagsLogik "Erwachsener" mit seiner Erfahrung("Rechnen()") schneller sein als "Kind" mit "Zählen()".

    Jetzt weis aber der Compiler nicht worauf es mir ankommt, und wenn er mein Funktionen ändert, zugunsten der Preformenz teilweise sogar unterschiedliche Aufgaben gleich übersetzt, ist am Ende womöglich sogar "Erwachsener" mit seinem "umständlichen" "Rechnen()" langsamer als "Kind" mit "Zählen()".
    (Deswegen "Kind" zählen zu erschweren [zwischen zwei Zahlen immer eine pause einlegen] wäre in dem Fall zwar eine Korrekturmöglichkeit, eine Lösung, die sich aus der Natur der Sache ergibt, wäre mir jedoch lieber. Und wichtiger, ohne Compilerwisen muß man erstmal darauf kommen, daß so ein Händikap notwendig ist.)



  • @axam Irgendwie überlegst du an komischen Sachen 😃

    Normalerweise ist man an der schnellsten Lösung zu einem Problem interessiert. Wenn du eine zeitabhängige Simulation machen möchtest, musst du dich auch selbst um die Zeitabhängigkeit kümmern.

    Ansonsten kannst du dem Compiler natürlich verbieten Optimierungen vorzunehmen. Wenn du dir den Godbolt Link von @wob anguckst, gibt es da das Flag -O2, wenn du daraus -O0 machst, wird nicht mehr optimiert.

    Aber ich möchte dich bitten, dir nicht anzugewöhnen mit -O0 für echten produktiven Code zu kompilieren 😉



  • @Schlangenmensch

    @Schlangenmensch sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:
    Ansonsten kannst du dem Compiler natürlich verbieten Optimierungen vorzunehmen. Wenn du dir den Godbolt Link von @wob anguckst, gibt es da das Flag -O2, wenn du daraus -O0 machst, wird nicht mehr optimiert.

    Genau deshalb war ja auch meine Frage:

    @axam sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:
    Gibt es eine Möglichkeit wie ich Funktionen irgend wie kennzeichnen kann, so daß sie vom Optimierungsprozess ausgenommen sind?

    So wie ich das Verstanden habe deaktiviere ich mit dem Flag -O0 die Optimierung global für den gesamten Compelierungssprozes. Adlerdings soll der Compiler ja seine Aufgabe bestmöglich machen, lediglich ausgewählten Funktionen würde ich gerne mit einer CompilerInformation makieren, damit der Compiler diese Stellen "wortwörtlich" übersetzt, schon allein um sicher sein zu können, daß durch das Compelieren kein unerwartetes "Verhalten" entsteht [wie etwa "Rechnen()" dauert länger als "Zählen()"].



  • @Schlangenmensch sagte in [Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise]

    wenn du daraus -O0 machst, wird nicht mehr optimiert.

    Niemand garantiert, dass bei -O0 gar keine Optimierungen stattfinden. "Einfachste" Optimierungen passieren trotzdem (was "einfachst" ist, ist abhängig vom Compilerhersteller).

    Aber ich möchte dich bitten, dir nicht anzugewöhnen mit -O0 für echten produktiven Code zu kompilieren 😉

    !



  • Implementiere das Rechnen selber mit Strings statt int.



  • @axam sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:

    Adlerdings soll der Compiler ja seine Aufgabe bestmöglich machen, lediglich ausgewählten Funktionen würde ich gerne mit einer CompilerInformation makieren, damit der Compiler diese Stellen "wortwörtlich" übersetzt, schon allein um sicher sein zu können, daß durch das Compelieren kein unerwartetes "Verhalten" entsteht [wie etwa "Rechnen()" dauert länger als "Zählen()"].

    Du kannst diese Funktionen in einer separaten Datei übersetzen und dort -O0 nehmen, für den Rest -O2. Oder auch deine Variablen als volatile deklarieren... Oder den Algorithmus kompliziert genug machen, sodass er nicht mehr automatisch optimiert wird.



  • @wob sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:
    Du kannst diese Funktionen in einer separaten Datei übersetzen und dort -O0 nehmen, für den Rest -O2. Oder auch deine Variablen als volatile deklarieren... Oder den Algorithmus kompliziert genug machen, sodass er nicht mehr automatisch optimiert wird.

    Das mit dem Auslagern der Entsprechenden Funktionen hab ich auch schon überlegt.
    volatile klingt vielversprechend (weiß zwar nicht was das bedeutet, werd' ich mir jedoch nachher mal aber näher ansehen).
    Funktionen unnötig kompliziert machen, erscheint mir jedoch wenig sinnvoll, und wäre mir auch zu unsicher (schließlich ist das "Endresultat" dann immer noch "ungewiß").



  • @axam Ich glaube, du solltest vor allem vorm Programmieren überlegen, was genau dein gewünschtes Ergebniss ist und dann ein Programm schreiben, dass dir das berechnet.

    Du kannst, um das Verhältnis Kind <-> Erwachsener in deinem Beispiel wiederzuspiegeln z.B. benötigte Rechenschritte mitzählen. Erwachsener: 1 Kind: kommt drauf an wie weit hoch gezählt werden muss. Und schon kannst du den Unterschied auch simulieren, ohne dass du da irgendwelche künstlichen Schranken auf Compiler Ebene anlegen musst.

    @wob Stimmt, -O0 garantiert lediglich, dass man beim debuggen, die Werte angezeigt bekommt, die man an der Stelle im Source Code erwartet.



  • @axam sagte in Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise:

    volatile klingt vielversprechend

    Naja, war eigentlich nur halb ernst gemeint. Normalerweise solltest du das nicht benutzen, wenn du nicht ganz genau weißt, warum du es brauchst. Schritte mitzählen, was Schlangenmensch vorgeschlagen hat, klingt vernünftiger.



  • Ok, das mit dem counten wäre eventuel eine Möglichkeit.
    Man betrachtet das "berechnet" Ergebnis als unabhängigen Wert, zählt dabei die Rechenschritte, die für die Berechnung vorgesehen wärden, und läst dann das Objekt entsprechend der counterZahl warten. Macht das ganze zwar etwas "langsamer" aber dafür ist man auf der sicheren Seite.
    Erwachsener ist übrigens nicht automatisch counterZahl=1 für zB. 44+44 bräuchte er min3 Schritte (für einen genauen Wert müsste ich es mir jedoch im Detail anseh'n wie viele Schritte es dann wirklich sind).



  • volatile bedeutet, dass die Lese- und Schreibzugriffe auf entsprechend qualifizierte Speicherstellen mit zum beobachtbaren Verhalten des Programms gezählt werden. Der Compiler kann also z.B. hier:

    volatile int x = 10;
    x = 10;
    int y = x;
    cout << x << y;
    

    nicht die Zuweisung (nach der Initialisierung) an x weglassen, er darf auch nicht einfach annehmen, dass y den Wert 10 hat, sondern muss den Wert von x tatsächlich nochmal aus dem Speicher lesen. Das ist zwar eigentlich für Hardware-Zugriffe und ähnliches gedacht, kann aber auch als Optimierungsbremse missbraucht werden.



  • Hatte jetzt Zeit mir mal "volatile" ein wenig näher anzusehen (an der Stelle auch dank an @Bashar für deine Zusamenfassung) und hab folgendes herausgefunden:
    Die meisten (u.a. auch Wikipedia) schreiben daß es genau dafür da ist was ich bräuchte, bzw. dafür verwendet wird.

    Was die Nachteile von "volatile" betrift:

    1. Varible belegt RAM (begrenzt akzeptabel)
    2. Langsamer, da zugriff über RAM erfolgt
    3. Langsamer, da Variable bei jedem Aufruf neu aus dem RAM geladen wird.
      Ob jetzt "4) keine/eingeschränkte Codeoptimierung" ein Vor- oder Nachteil ist hängt von der jeweiligen Situation ab.

    Letzten Endes würde also "volatile" das machen was ich bräuchte, die Nachteile sind jedoch ... Nur mal laut gedacht: Ich frage mich ob der Compiler meine Funktion auch in ruhe läst wenn in der Funktion ein "volatile int null" steht. Wüste aber nicht so recht was ich mit "null" machen soll. Steht in der Schleife "volatile int null;" erfolgt bei jedem Durchlauf eine neue initalisierung, bei "null=null;" erfolgt ein Speicherzugriff, eben so bei "null=0;" und mit if(null==null) ist es vermutlich auch nicht besser.
    Schreibe ich einfach nur "null;" so erzeugt dies zumindest in VS2019 erstmal keine Warnung/Fehlermeldung. Ob es auch so beim Compelieren wäre und ob ein "null;" in der for-Schleife den gewünschten Effekt hätte (keine Änderung der Funktion durch den Compiler und trotzdem kein Speicherzugriff auf "null") kann ich jedoch nicht beurteilen.

    Übrigens hab ich während meiner Suche auch eine witzige Atwort auf die Frage "Why do we use volatile..." gefunden, die ich euch nicht vorenthalten will:

    In other words, I would explain this as follows:
    volatile tells the compiler:

    "Hey compiler, I'm volatile and, you know, I can be changed by some XYZ that you're not even aware of. That XYZ could be anything. Maybe some alien outside this planet called program. Maybe some lightning, some form of interrupt, volcanoes, etc can mutate me. Maybe. You never know who is going to change me! So O you ignorant, stop playing an all-knowing god, and don't dare touch the code where I'm present. Okay?"

    Well, that is how volatile prevents the compiler from optimizing code.

    "smile"😁

    Nachtrag:
    Eventeull wäre es ja eine Lösung wenn ich beim Funktionsaufruf "volatile" benutze?
    V1:

    volatile int plus(int w1, int w2)
    

    Mögliches Problem:
    Gesetzt dem Fall dies wäre Möglich, dann wäre der return-Wert ja vermutlich ein volatile, was dann im weiteren Verlauf zu Problemen führen könnte?

    V2:

    int plus(int w1, int w2, volatile int null)
    

    Hier wird beim Funktionsaufruf "0" als driter Wert an die Funktion übergeben.
    "null" findet in der Funktion selbst keine Verwendung ergo höchstens 1 Speicherzugriff (= Initialisierung)?



  • Wie wäre es, wenn Du Dich mit Assembler beschäftigst und lernst wie der Computer direkt funktioniert? Sinnvoll wäre ein Microcontroller. Ein SingleBoard Computer wie ein Pi hat schon zuviel OS, so dass das die eigentliche Funktionsweise verdeckt. Aber auch damit geht das. Nur sind halt moderne OS deutlich komplizierter als alles was zu Anfang der Heimcomputerzeit gab. Vielleicht wäre RiscOS auf einem Pi noch weniger kompliziert.



  • @john-0 Hab ich auch schon mal überlegt. Nachteil:

    1. Assembler/Maschinencode muß meines wissens nach individuell für jede Hardware geschrieben werden.
    2. Größere Projekte können mit Assembler/Maschinencode nicht realisiert werden.

    Sofern sich dein Tipp auf die Ursprüngliche Fragestellung bezieht, es wurde bereits darauf hingewiesen, das es keine C++Liste geben kann, da es keine eindeutige interne Funktionsweise von C++Befehlen gibt. Jetzt geht es eher darum, wie ich dem Compiler sagen kann was ich eigentlich will, warum ich eine Funktion so geschrieben habe, wie ich sie geschrieben habe, damit er den Code dann dahingehend optimiert, das die Funktion am Ende immer noch das tut wofür ich sie geschrieben habe. Zum Beispiel geht es mir bei der von mir geschriebenen plus-Funktion eben nicht nur um den return-Wert (sonst würde ich ja enfach "+" nehmen und nicht weiter darüber nachdenken), sondern auch darum wie der return-Wert erzeugt wird.



  • Assembler zu verstehen ist sehr nützlich. Dabei ist auch weitgehend egal, welches Assembler.
    Wenn du dich vor allem für PC´s interessierst, ist x86 Assembler völlig ausreichend.



  • @axam
    Ich kenne keine Möglichkeit einen Compiler zu zwingen ein Zwischenergebnis auch wirklich zu berechnen, ohne dass man es in einer Art und Weise verwendet die er nicht wegoptimieren kann oder darf.
    Sich auf "kann nicht" zu verlassen ist meist riskant -- neuere Compiler-Versionen könnten ja besser optimieren können. Und alle Möglichkeiten für "darf nicht" die ich kenne kommen mit zusätzlichen Kosten.

    Die billigste Variante die ich finden konnte ist das Zwischenergebnis in eine lokale volatile Variable zu speichern:

    int add(int w1, int w2) {
        volatile int dummy;
        for (int i = 0; i < w2; i++) {
            w1++;
            dummy = w1;
        }
        return w1;
    }
    

    https://godbolt.org/z/Psj76d

    Der generierte Code ist besser (läuft schneller) als wenn du w1 oder w2 volatile machst, da der Compiler dabei nicht gezwungen wird den Wert jedes mal wieder aus dem Speicher zurückzulesen.



  • Hatte gerade in gewisser Weise ein umdenken.
    Im Grunde könnte das Ganze auch hierarchisch gelöst werden.
    Sprich, ich greife zwar den Vorschlag mit dem "Counten" auf, jedoch ist, anstelle einer Zeitverzögerungung, die Counterzahl eine fixe Variable des Objekts vomTyp "Uhr" - sowas wie eine Ordnungszahl. Counterzahlen des selben Objekts werden addiert. Anstelle das Objekt warten zulassen, werden niedere Ordungszahlen "vorgereit". Unabhängig von der tatsächlichen Laufzeit wäre eine Mögliche Ausgabe: "Erwachsener A hat gewonnen. Er hat dafür 3ms benötigt. Kind A hat für die selbe Aufgabe 44ms gebraucht." Bei Bedarf kann man dann die Uhren immer noch "synchronisieren" also alle Objekte "warten lassen" bis sie auf 44ms sind --- oder man ändert einfach den Wert "Uhr" auf 44ms. Auf diese Weise bräuchte man auch keine komplizierte Funktion, sondern nur zB. bei Kind CounterZahl=w2;

    Einziger Wermutstropfen daran, es ist eben nur ein workaround. Es wird dadurch nur dieses eine konkrete Problem gelöst. Ich werde jedoch trotzdem auch weiterhin aufgrund von Übersetzungsfehlern über "merkwürdiges" Verhalten meines Programms stolpern und meistens nie erfahren, was genau die Ursache ist - den im konkreten Fall war es offensichtlich wo der Fehler liegt und wie er entsteht, bei größeren Projekten könnte der Übersetzungsfehler jedoch überall entstehen, da die Funktionen ja scheinbar die "richtigen" Ergebnisse liefern, jedoch zum Beispiel das Verhalten einzelner Funktionen zueinander anders ist als vom Programmierer (also von mir) vorgesehen, oder einfach irgend etwas anderes nicht stimmig ist. Man kann also lediglich für die Symptome (wenn sie den regelmässig auftreten und der Fehler reproduzierbar ist) einen workaround "basteln", und selbst dieses "workaround" kann dann neu Fehler produzieren.
    Und ich fürchte Asymbler und Grundverständnis wird mir da auch nicht wirklich viel weiter helfen (beispielsweise weis ich dank Grundverständnis jetzt, das der Compiler den fehlerhaften Code erzeugt bzw. die Ursache dafür ist, aber so wirklich ändern kann ich daran eben nichts) es seiden ich würde dann tatsächlich auch noch meinen eigen Compiler schreiben, was dann von Aufwand und Zeit weit über das Ziel hinaus schießt - ganz abgeseh'n davon, daß ich das womöglich gar nicht könnte, oder besten falls, wenn ich mich auf eine ganz konkrete Hardware festlege und meine Programme nur für meine eigene Hardware schreibe in der Hoffnung das ich nie wieder neue Hardware brauche, was in der Realität dann doch eher so ein kleines biiischen Unrealistisch ist. >ironieOFF<
    Zumindest ist dies mein derzeitiges Fazit aus dem Ganzen.


Anmelden zum Antworten