Informationen finden zu: BasicLowLevel C++ / Interne Funktionsweise



  • 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.



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

    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

    Was ist denn dein Problem?

    So wie du schreibst könnte man meinen, du hast ein Programm mit vielen Threads und ab und zu sind deine Daten zerschossen.



  • @axam Du kannst einfach kaum Ahnnahmen darüber treffen, welche Berechnungen auf einem beliebigen System wie lange brauchen.
    Möglicherweise kann dein nächstes System superschnell zählen, weil es da eine neue Hardware für gibt, und zack schon sind deine Annahmen kaputt.

    Und auch genereller und weniger theoretisch. Es ist kaum vorherzusehen, was dein Processor an Branch Prediction, Threading, Paging und was sonst noch alles eine Rolle spielt, alles macht. Oder welche Aufrufe dein OS noch dazwischen schiebt.
    Wenn du zeitliche Abläufe simmulieren willst, muss du das selber tun. Wenn du Abläufe synchroniesieren musst, weil Aufgabe A vor Aufgabe B fertig sein muss, dann musst du das selber tun. Du kannst dich nicht darauf verlassen, dass Berechnung A schneller ist als B und das dass immer so bleibt.

    Was Assembler betrifft, da wärst du wahrscheinlich mit x86 bzw x86-64 ganz gut bedient und die Wahrscheinlichkeit ist hoch, dass auch dein nächster PC das noch versteht.



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

    @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.

    Es geht darum zu verstehen, wie der Computer funktioniert. Es geht rein um Lerneffekte und nicht darum ein Programm wie MS Word mit Assembler nachzuprogrammieren. So etwas hat man schon zur Heimcomputerzeit auf einem Atari ST oder Amiga nur noch selten gemacht, in der Regel wurden die Programme damals schon in C oder einen ähnlichen Sprache geschrieben. Auf den 8Bit Heimcomputer sah das vielfach noch anders aus, da wurde in der Hauptsache Z80, 6502 oder 6800 Assembler genutzt.

    Es geht einfach darum, dass Du einige Verständnisprobleme hast, wie ein Computer funktioniert. Das kannst Du mit dem Erlernen von Assembler beheben. Als Plattform würde ich persönlich ARM x86-64 vorziehen, weil letztere ein ziemliches Stückwerk an Assembler nutzt. Man merkt es x86-64 einfach an, dass das einmal aus einem 8Bit Prozessor entwickelt wurde.


  • Mod

    Sogar irgendeiner der vielen Assembler-Simulatoren würde sehr beim Verständnis helfen. Vielleicht sogar mehr noch als das echte Ding, weil man Schritt für Schritt zugucken kann, was passiert. Ist aber weniger spannend als das Experiment am echten System.



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

    Sogar irgendeiner der vielen Assembler-Simulatoren würde sehr beim Verständnis helfen. Vielleicht sogar mehr noch als das echte Ding, weil man Schritt für Schritt zugucken kann, was passiert. Ist aber weniger spannend als das Experiment am echten System.

    Dann böte sich sogar MMIX an, wenn man gewillt ist sich durch Knuths Werk zu arbeiten.



  • @Schlangenmensch
    Im Grunde habe ich genau dies, dank euch allen inzwischen gelernt.
    Ich kann mich also mehr oder weniger darauf Verlasen das bei von mir gewünschten Berechnungen der "return-Wert" das richtige Ergebnis liefert, ich werde jedoch niemals wissen, was mein Programm genau macht.

    Beispielsweise kann ich niemals einfach eine Schleife programmieren und mich dann darauf verlassen das diese so und so oft aufgerufen wird. Ergo sind Funktionen in ihrer Effizient für mich auch nicht direkt vergleichbar.
    Ich kann daher niemals mit Gewissheit sagen, wenn ich Funktion A so oder so gestalte und dann Funktion B aufrufe, dann ist das ganze effizienter/schneller als wenn ich beide Funktion durch Funktion C ersetze, die dafür doppelt so viele Schritte benötigt, da es ja sein könnte, das der Compiler Funktion C mit einer Multiplikation löst, während er bei den Funktionen A-B gezwungen ist zwei Additionen auszuführen. (Vereinfacht gesagt)
    Und wenn eine Funktion auch von ihrer Funktionsweise abhängig ist, sollte ich besser mal meinen gewünschten return-Wert hinterfragen und die Funktion umschreiben/aufsplitten.

    Ich Danke euch allen für diese Erkenntnis.
    Und auch dafür, daß ich, dank euch jetzt erkannt habe, daß ich mehr Augenmerk darauf legen muß, was genau ich als return-Wert haben will, anstatt meine Zeit damit zu verschwenden, mir Gedanken darüber zu machen wie ich das Programm am besten optimiere, indem ich den gewünschte return-Wert auf diese oder jene Weise berechne, damit dies dann auch am besten/effizientesten Weg geschieht. Denn dies wäre ja nur der beste/effizienteste Weg, wenn das Programm in allen von mir bedachten Fällen so übersetzt würde, wie es von mir ursprünglich gedacht war.
    DANKE

    PS.: MMIX klingt interesant. Muß zwar erstmal 'ne Nacht drüber schlafen, denke aber, ich werd mir das Buch kaufen.



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

    Ich kann mich also mehr oder weniger darauf Verlasen das bei von mir gewünschten Berechnungen der "return-Wert" das richtige Ergebnis liefert

    Ich kann mich also mehr oder weniger immer darauf verlassen, dass bei von mir gewünschten Berechnungen der "return-Wert" das richtige Ergebnis liefert.



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

    Ich kann mich also mehr oder weniger immer darauf verlassen, dass bei von mir gewünschten Berechnungen der "return-Wert" das richtige Ergebnis liefert.

    OFF-Topic:
    Dazu ein kleiner Witz am Rande:

    Ein Astrologe, ein Physiker und ein Mathematiker fahren mit dem Zug von England nach Schottland.
    Das erste Schaf das der Astrologe in Schottland sieht ist schwarz und er ruft ganz erstaunt aus:
    "Seht mal, in Schottland sind alle Schafe schwarz."
    Darauf der Phsiker: "Eigentlich wissen wir jetzt nur, das es in Schottland schwarze Schafe gibt."
    Meint der Mathematiker:
    "Also, das ist so nicht ganz korrekt.
    Wir wissen jetzt lediglich,
    daß es in Schottland eine Weide gibt,
    auf der ein Schaf steht,
    das zumindest auf einer Seite schwarz ist."

    😁



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

    Und wenn eine Funktion auch von ihrer Funktionsweise abhängig ist, sollte ich besser mal meinen gewünschten return-Wert hinterfragen und die Funktion umschreiben/aufsplitten.

    Ich verstehe nicht ganz was du meinst. Kannst du ein Beispiel liefern für eine Funktion die "abhängig von ihrer Funktionsweise" (meinst du damit abhängig davon wie der Compiler sie übersetzt?) unterschiedliche Ergebnisse liefern/unterschiedliche Effekte haben könnte?



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

    Ich verstehe nicht ganz was du meinst. Kannst du ein Beispiel liefern für eine Funktion die "abhängig von ihrer Funktionsweise" (meinst du damit abhängig davon wie der Compiler sie übersetzt?) unterschiedliche Ergebnisse liefern/unterschiedliche Effekte haben könnte?

    Dann hätte er UB ins Programm eingebaut, was man normalerweise explizit zu verhindern sucht.

    Er hat ein ziemlich großes Verständnisproblem, wenn er solche Dinge in Betracht zieht.



  • Was schnell oder langsam ist, hängt noch von soviel mehr ab als den konkreten Anweisungen. Selbst mit zig Jahren Erfahrung fällt es schwer C++ Code auch nur grob nach Performance zu ordnen. Man sollte das daher überhaupt nur angehen, wenn man mit einer Messung bewiesen hat, das die Performance nicht ausreicht.



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

    Was schnell oder langsam ist, hängt noch von soviel mehr ab als den konkreten Anweisungen. Selbst mit zig Jahren Erfahrung fällt es schwer C++ Code auch nur grob nach Performance zu ordnen. Man sollte das daher überhaupt nur angehen, wenn man mit einer Messung bewiesen hat, das die Performance nicht ausreicht.

    Was man meines Erachtens relativ einfach sagen kann, dass man Algorithmen auf modernen CPUs auf Cache Hits optimieren muss. Nichts ist schlimmer, als wenn die CPU auf den Arbeitsspeicher warten muss. Alle Mikrooptimierungen hingegen sollte man nur angehen, wenn man mit dem Profiler konkrete Werte hat und auch vorher schon weiß, dass sich das lohnt.



  • @hustbaer
    Ich kann dir leider kein brauchbares Beispiel geben, denn es ist so wie @john-0 andeutet, ich habe "ein ziemlich großes Verständnisproblem".

    Also wenn ich in meinem Code sage + - schleife, usw. kann ich aus meiner beschränkten sequenziellen Sicht heraus geistig nachvollziehen was genau mein Programm macht, welche Ergebnisse ich dort und da erwarte, und für mich sind es n-Schritte, die für eine Lösung nötig sind.
    Da der Compiler jedoch den von mir geschriebenen Code verändert bin ich gezwungen, daß ich darauf vertraue, daß der Compiler tatsächlich alles richtig macht.
    Sprich es besteht zumindest theoretisch die geringe Möglichkeit, daß ich im Code einen Fehler nicht finden kann, da das Programm fehlerfrei funktionieren würde wenn der Code 1:1 vom Compiler übersetzt wird.

    Ein dummes Beispiel aus der Realität:
    "4 mal 6" und "6 mal 4" sind in der Mathematik äquivalent, also austauschbar.
    Wenn ich nur wissen will, wie viele Äpfel ich habe, sind es immer 24, es macht semantisch jedoch einen unterschied ob ich 4 Packungen habe mit je 6 Äpfeln, oder 6 Packungen mit je 4 Äpfeln.
    Und selbst wenn ich mich darauf festlege, das in einer Packung 6 Äpfel sind, ist da immer noch ein kleiner Unterschied ob ich nun 4 Packungen nehme und mir ausrechne das ich jetzt 24 Äpfel habe (4x6), oder ob ich sehe das in einer Packung 6 Äpfel sind und mir ausrechne das ich bei 4 Packung 24 Äpfel habe (6x4), was der Compiler dann womöglich sogar (übertrieben gesagt) mit einer Division löst (24/6) wo es dann sehr wohl einen unterschied machen würde welcher Wert zuerst steht.
    Gut der letzte Teil mit dem Compiler ist ein wenig an den Haaren herbeigezogen, und es stellt sich auch die Frage wie weitreichend ist die Unterscheidung zwischen "4 mal 6" und "6 mal 4" tatsächlich, allerdings sagte ich bereits am Anfang, es sei ein dummes Beispiel.

    Aber es bleibt jedoch, ab dem Punkt des Compilieren verliere ich die Kontrolle über den Code. Ein Fehler resultiert somit nicht nur ausschliesslich aus einer fehlerhaften Logik meines Codes, sonder es besteht immer auch die Möglichkeit, das der Code für den Compiler einfach nur falsch "formuliert" ist.


Log in to reply