C-Kurs, Abend 6 - Verbesserungsvorschläge


    • skoe
    • 27894 Aufrufe 83 Antworten

    Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

    • Bombjack schrieb:

      Hi skoe,

      bin gerade auf den c kurs gestossen, ganz tolle sache!!

      sehr schade dass er aufhört gerade da wo es richtig spannend wird. würde mich tierisch freuen wenn es eine fortsetzung gäbe welche die weiteren bibliotheken des cc65 erklärt, insbes sprites, scrolling und musik!! falls es so etwas gibt

      auch wie man leveldaten, sprites und charsets von diksette nachlädt würde mich interessieren

      viele grüsse


      cc65 Pointer Arithmetik

      In dem zip File sind ein paar Include Files. Vielleicht hilft Dir das schon.
      Hier könnte ihre Signatur stehen!
    • marvin schrieb:

      delay() verzögert einen unbekannten Bruchteil einer Sekunde, ich habs nie exakt nachgemessen :whistling:
      Mit einem entsprechend guten Compiler wird das schiefgehen, der optimiert die Schleife nämlich weg. Mag sein, dass cc65 das nicht tut, aber für die Zukunft wollte ich es mal anmerken. ;)
      Verzögerungen mit "Leerschleifen" zu erreichen ist natürlich grundsätzlich problematisch - nimm lieber eine externe Referenz, also einen CIA-Timer oder das Rasterregister des VIC, je nach gewünschter Granularität.

      ...und auch wenn man es niemals tun sollte - der Vollständigkeit halber (ist ja schließlich der C-Kurs hier :)) hier noch die Erklärung, wie es auch mit Leerschleifen geht: Deklarier den Schleifenzähler als "volatile" - dann darf der Compiler keinen Zugriff darauf mehr wegoptimieren.
      Yes, I'm the guy responsible for the ACME cross assembler
    • Mac Bacon schrieb:

      Mit einem entsprechend guten Compiler wird das schiefgehen, der optimiert die Schleife nämlich weg. Mag sein, dass cc65 das nicht tut, aber für die Zukunft wollte ich es mal anmerken. ;)

      Genau, da kommt es eben wieder darauf an, ob man wirklich C programmieren will (keine sichtbaren Seiteneffekte an allen Sequenzpunkten -> Schleife kann weg) oder einen "maschinenfixierten C-Dialekt" ;) cc65 optimiert sehr wenig, daher auch diese Seite in der Dokumentation: oliverschmidt.github.io/cc65/doc/coding.html.

      Mac Bacon schrieb:

      Verzögerungen mit "Leerschleifen" zu erreichen ist natürlich grundsätzlich problematisch - nimm lieber eine externe Referenz, also einen CIA-Timer oder das Rasterregister des VIC, je nach gewünschter Granularität.

      full ack

      Mac Bacon schrieb:

      ...und auch wenn man es niemals tun sollte - der Vollständigkeit halber (ist ja schließlich der C-Kurs hier :)) hier noch die Erklärung, wie es auch mit Leerschleifen geht: Deklarier den Schleifenzähler als "volatile" - dann darf der Compiler keinen Zugriff darauf mehr wegoptimieren.

      Und hier noch kurz, warum man das nicht tun sollte: Es ist eine Zweckentfremdung von "volatile". Volatile ist für Variablen, die sich außerhalb der Kontrolle des compilierten Programms ändern. Typischerweise memory mapped IO, also z.B.

      Quellcode

      1. volatile char *vic_raster_low = (char *)0xd012;

      (auch nicht mit dem cc65 so machen, siehe link oben, aber das wäre eine nach C standard korrekte Verwendung von volatile)
    • Danke für die Erklärung zu "volatile", die hätte ich in der Tat noch mitliefern sollen. Aber wo siehst Du das Problem darin, dem Compiler eine I/O-Speicherstelle vorzugaukeln wo keine ist, wenn man genau dieses Ziel erreichen will? Beispiel:

      Quellcode

      1. volatile char *ti_low = (char *) 0xa2;
      2. void wait_for_interrupts(int count)
      3. {
      4. char value;
      5. while (count--) {
      6. value = *ti_low;
      7. while (value == *ti_low)
      8. ;
      9. }
      10. }
      Alles anzeigen
      Diese Funktion wartet eine wählbare Anzahl von Interrupt-Zyklen. "volatile" wird hier völlig korrekt benutzt, nämlich für eine Speicherstelle, die vom System-Interrupt verändert wird. Ohne "volatile" würde dieser Code in eine Endlosschleife münden, wenn der Compiler nur gut genug optimiert. Beispiel zwei:

      Quellcode

      1. volatile char *ti_low = (char *) 0xa2;
      2. int count_interrupts(int tries)
      3. {
      4. char value = *ti_low,
      5. changed = 0;
      6. while (tries--) {
      7. if (value != *ti_low) {
      8. value = *ti_low;
      9. ++changed;
      10. }
      11. }
      12. return changed;
      13. }
      Alles anzeigen
      Diese Funktion überprüft "tries"-mal, ob ein Interrupt aufgetreten ist, und gibt die Anzahl Interrupts zurück (wofür das gut ist, sei einmal dahingestellt :whistling: ). Da der Compiler hier nichts wegoptimieren darf, könnte man diese Funktion als Delay-Schleife benutzen (auch wenn das, wie bereits erklärt, wirklich nicht sehr sinnvoll ist).
      Wenn man nun den Rückgabewert eh ignoriert, kann man auch jede andere Speicherstelle "überprüfen", egal ob sie wirklich "volatile" ist oder nicht.
      Yes, I'm the guy responsible for the ACME cross assembler
    • Mac Bacon schrieb:

      Aber wo siehst Du das Problem darin, dem Compiler eine I/O-Speicherstelle vorzugaukeln wo keine ist, wenn man genau dieses Ziel erreichen will?


      Hallo :) Damit sehe ich überhaupt kein Problem, denn das ist ja auch kein "vorgaukeln". Ich schrieb "typischerweise" mmapped I/O .. nicht notwendigerweise. Wichtig ist, dass die Speicherstelle außerhalb der Kontrolle des Programms selbst geändert wird, das ist ja in deinen Beispielen der Fall. Das ist zum Beispiel deshalb so wichtig, weil immer wieder Leute zu der falschen Annahme gelangen, volatile sei für Thread-Synchronisation brauchbar. Aber auch eine Variable, die vollständig unter eigenen Kontrolle steht, als volatile zu deklarieren, ist bis auf wenige Ausnahmen (sjlj und unix signal handling) ein Fehler. Man verhindert künstlich gewisse Optimierungen, was darüber hinwegtäuscht, dass das Programm eigentlich nicht korrekt ist (In der Definition von C gibt es keine Ausführungszeit, nur Sequenzpunkte und Seiteneffekte).

      Abgesehen davon ist gerade auf dem C64 die aktive Verschwendung von Cycles fürs Timing ein sehr fragwürdiger Ansatz ;) Wie du schon schriebst: stattdessen Interruptquellen verwenden.
    • Ich bezog mich nicht auf den Unterschied zwischen I/O-Adressen und vom-Interrupt-geänderten-Speicherstellen - da hätte ich mich wohl besser ausdrücken sollen.
      Mein Punkt ist, dass Beispiel zwei auch dann funktioniert, wenn sich die Speicherstelle gar nicht ändert. Also könnte man genausogut auch jede andere Speicherstelle nehmen; eben auch eine, die in Wirklichkeit gar nicht "volatile" ist. Die Deklaration als "volatile" dient dann eben nur dazu, die Compiler-Optimierungen zu unterdrücken. Meine Frage ist nun, warum das Programm dann "eigentlich nicht korrekt" sein soll?
      Und warum sollte "volatile" nicht für Thread-Synchronisation brauchbar sein? Klar, normalerweise sind dafür Library-Funktionen bzw. Semaphoren/Mutexe/etc. vorhanden. Aber das ändert doch nichts daran, dass es auch mit "volatile" ginge.
      Yes, I'm the guy responsible for the ACME cross assembler
    • Mac Bacon schrieb:

      Also könnte man genausogut auch jede andere Speicherstelle nehmen; eben auch eine, die in Wirklichkeit gar nicht "volatile" ist. Die Deklaration als "volatile" dient dann eben nur dazu, die Compiler-Optimierungen zu unterdrücken. Meine Frage ist nun, warum das Programm dann "eigentlich nicht korrekt" sein soll?

      Weil volatile nur heißt, dass die Variable bei jedem Lesezugriff tatsächlich aus dem Speicher gelesen werden muss. Es gibt aber keine Aussage darüber, WANN dieser Zugriff zu geschehen hat. Umsortierungen des Optimizers sind nach wie vor erlaubt, nur verschiedene volatile-Zugriffe müssen in der vorgegebenen Reihenfolge erfolgen.

      Damit ist natürlich klar, dass eine Warteschleife mit einer volatile Laufvariable funktionieren wird. Murks ist es trotzdem, und falls man in der Schleife noch irgendetwas anderes tut ist es gut möglich, dass das Resultat vom erwarteten abweicht. Außerdem wird volatile wie gesagt zweckentfremdet -- ich nenne das eben kein korrektes C sondern einen hack :)

      Mac Bacon schrieb:

      Und warum sollte "volatile" nicht für Thread-Synchronisation brauchbar sein? Klar, normalerweise sind dafür Library-Funktionen bzw. Semaphoren/Mutexe/etc. vorhanden. Aber das ändert doch nichts daran, dass es auch mit "volatile" ginge.

      Thread-safety ist generell ein Minenfeld -- in Sprachen, die keine Threads kennen (C) ganz besonders ;) Die falsche Verwendung von volatile zu diesem Zweck ist leider weit verbreitet, sicher auch, weil MSVC volatile so behandelt, dass es korrekt funktioniert -- aber das ist eine Ausnahme, mal mit nem anderen Compiler bauen und man hat plötzlich "lustige" Bugs. Für eine ausführliche Erklärung siehe z.B. die beste Antwort zu dieser Frage:
      stackoverflow.com/questions/24…readed-c-or-c-programming
    • Zirias schrieb:

      Mac Bacon schrieb:

      Also könnte man genausogut auch jede andere Speicherstelle nehmen; eben auch eine, die in Wirklichkeit gar nicht "volatile" ist. Die Deklaration als "volatile" dient dann eben nur dazu, die Compiler-Optimierungen zu unterdrücken. Meine Frage ist nun, warum das Programm dann "eigentlich nicht korrekt" sein soll?

      Weil volatile nur heißt, dass die Variable bei jedem Lesezugriff tatsächlich aus dem Speicher gelesen werden muss. Es gibt aber keine Aussage darüber, WANN dieser Zugriff zu geschehen hat.[...] Damit ist natürlich klar, dass eine Warteschleife mit einer volatile Laufvariable funktionieren wird. Murks ist es trotzdem [...] Außerdem wird volatile wie gesagt zweckentfremdet -- ich nenne das eben kein korrektes C sondern einen hack :)

      Diese beiden Kategorien sind doch gar nicht disjunkt, siehe Duff's Device. :)
      Das Problem, das ich mit dieser Einteilung in Murks und korrektes C habe, ist Folgendes:

      Quellcode

      1. volatile char *test = (char *) 0xa2; // low byte of TI
      2. //volatile char *test = (char *) 0xa3; // something else entirely
      3. void delay(int count)
      4. {
      5. char dummy;
      6. while (count--)
      7. dummy = *test;
      8. }
      Wird jetzt die erste Zeile auskommentiert und statt dessen die zweite aktiviert, verwandelt sich "korrektes C" plötzlich in "Murks", und das, obwohl die Funktion faktisch genau das Gleiche tut wie vorher (gut, sie ignoriert jetzt ein anderes Byte, aber ein Unterschied, der keinen Unterschied macht, ist kein Unterschied. ;))

      Zirias schrieb:

      Mac Bacon schrieb:

      Und warum sollte "volatile" nicht für Thread-Synchronisation brauchbar sein? [...]

      Die falsche Verwendung von volatile zu diesem Zweck ist leider weit verbreitet, sicher auch, weil MSVC volatile so behandelt, dass es korrekt funktioniert -- aber das ist eine Ausnahme, mal mit nem anderen Compiler bauen und man hat plötzlich "lustige" Bugs. Für eine ausführliche Erklärung siehe z.B. die beste Antwort zu dieser Frage:
      stackoverflow.com/questions/24…readed-c-or-c-programming

      Danke für den Link. Ich hab die letzten Tage eine Menge zu dem Thema gelesen, um meine offensichtlich vorhandenen Defizite diesbezüglich aufzuarbeiten. :D
      Zu meiner Verteidigung kann ich nur sagen, dass ich gar nicht an volle Synchronisation zwischen Threads über shared structs gedacht hatte, sondern nur an Signalling zwischen Vordergrundprogramm und verschiedenen Interrupt Handlern, aber das macht es nicht besser...
      Nur der Vollständigkeit halber: MSVC ist mir glücklicherweise bisher erspart geblieben, auch bei meinem Arbeitgeber kann ich zu 99% Linux und GCC benutzen.
      Yes, I'm the guy responsible for the ACME cross assembler
    • Mac Bacon schrieb:

      Quellcode

      1. volatile char *test = (char *) 0xa2; // low byte of TI
      2. //volatile char *test = (char *) 0xa3; // something else entirely
      3. void delay(int count)
      4. {
      5. char dummy;
      6. while (count--)
      7. dummy = *test;
      8. }

      Nur der Vollständigkeit halber und sicher nicht um jemanden anzugreifen: DAS würde ich in beiden Fällen Murks nennen. Warum? Ein vernünftiger Optimizer schmeißt die unbenutzte Variable "dummy" einfach raus. Übrig bleibt da ein NOP (oder auch nichts), und das fällt dann beim inlinen weg ;) Naja soweit zumindest meine Vermutung, mir ist nicht bekannt, dass C mit Seiteneffekten durch das LESEN einer (volatile) Speicherstelle rechnet -- habe jetzt aber nicht im Standard nachgeschlagen. Aber selbst wenn: wenn man gar nicht an dem Wert der Speicherzelle interessiert ist, ergibt "volatile" in C doch eher wenig Sinn. Und nicht zu vergessen: cc65 ist sehr mager, was Optimierungen angeht.

      Mac Bacon schrieb:

      Nur der Vollständigkeit halber: MSVC ist mir glücklicherweise bisher erspart geblieben, auch bei meinem Arbeitgeber kann ich zu 99% Linux und GCC benutzen.

      Gehört nur am Rande hier her, aber beruflich hab ich NUR mit Microsoft-Compilern zu tun, allerdings (zum Glück) nicht C sondern C#. In C# ist (wie auch in java) "volatile" ein wenig anders definiert und damit für manches korrekt, für das es in C eben falsch ist. Trägt durchaus zur allgemeinen Verwirrung bei ;)

      sauhund schrieb:

      einfach im kopf behalten "wenn es sich nicht um ein hardware register dreht, dann ist volatile fast immer falsch" :)

      Als Faustregel sehr gut brauchbar :) Übliche Ausnahmen wären wie gesagt sj/lj und unix signal handling, beim C64 sicher auch ZP-Speicherstellen, die das System schreibt.

    • Sehr guter Artikel zum Thema, definitiv empfehlenswert zum lernen :)

      - Den Typedef Vorschlag aus Abschnitt 3 finde ich nicht so toll. Ich weiß gar nicht, was da bei C so schwierig sein soll, type qualifier binden halt nach links, außer es gibt links nichts mehr (anfang der deklaration), DANN nach rechts.

      - Abschnitt 5, Absatz 6: Da bin ich anderer Meinung. Reordering über volatile hinweg ist korrekt und erwünscht, wenn es der Optimierung dient. Der ursprüngliche Zweck von volatile (mmapped I/O) wird davon ja nicht berührt.

      - Abschnitt 9 hat mich überrascht -- nicht, dass manche Versionen von GCC diesen Code wegschmeißen (der sehr an das Beispiel von Mc Bacon weiter oben erinnert), sondern dass das falsch ist. Aber in der Tat, mit den Zitaten aus dem Standard wird das klar. Damit hat also Mc Bacon recht, das Beispiel tut das, was man erwartet (jedenfalls so grob, seine "dummy" Variable dürfte trotzdem rausfliegen *g*)
    • Zirias schrieb:

      Ein vernünftiger Optimizer schmeißt die unbenutzte Variable "dummy" einfach raus.

      Ich habe das zweite Beispiel aus Post #64 auf den für meine Frage relevanten Teil reduziert, wollte es aber immer noch für C-Anfänger lesbar halten. Wer errät, warum ich die Variable "dummy" genannt habe, bekommt einen Preis... :whistling:

      Zirias schrieb:

      Übrig bleibt da ein NOP (oder auch nichts), und das fällt dann beim inlinen weg ;) Naja soweit zumindest meine Vermutung, mir ist nicht bekannt, dass C mit Seiteneffekten durch das LESEN einer (volatile) Speicherstelle rechnet -- habe jetzt aber nicht im Standard nachgeschlagen.

      In dem Fall wäre aber eindeutig der C-Standard Murks, denn read-sensitive-registers gibt es nicht nur in den CIAs und VDCs dieser Welt - und wenn man den Fall "ich muss einen Lesezugriff auf das I/O-Register machen, um einen Seiteneffekt auszulösen, aber der gelesene Wert interessiert mich nicht" nicht abbilden könnte, wäre C sehr kaputt.

      Zirias schrieb:

      Den Typedef Vorschlag aus Abschnitt 3 finde ich nicht so toll.

      Ja, zu typedef steht schon alles Wichtige in linux/Documentation/CodingStyle

      Da Du mein in Post #68 dargelegtes Problem elegant ignoriert hast, lassen wir's doch gut sein - keine Antwort ist auch ne Antwort. 8)
      Yes, I'm the guy responsible for the ACME cross assembler
    • Ich bin doch grad eben drauf eingegangen? Oder ich verstehe dein Problem nicht ?( "volatile" DAFÜR zu nutzen, dass als Nebeneffekt bestimmte Optimierungen wegfallen, ist eben falsch -- und das tun beide Varianten des Codes, unabhängig davon ob nun eine Speicherstelle missbraucht wird, die tatsächlich "extern" verändert wird.
    • enTHUSi schrieb:

      Schoen wie hier der Thread total verhunzt wird.
      VOLATILE ist exakt das womit man sich am 6. Abend mit C beschaeftigen sollte...


      Ok, alles ab Post #63 (bzw. #64, je nach Sichtweise) gehört eigentlich in einen eigenen "volatile"-Thread ausgelagert. Von mir aus kann der auch in die Laberecke, denn zwischen Zirias und mir wird es wahrscheinlich auf eine philosophische Diskussion über die Bedeutung der Worte "falsch" und "Du sollst nicht" hinauslaufen... :D

      sauhund schrieb:

      die frage ist: wie oft wird eigentlich "volatile" im trapthem source verwendet?

      Hoecker, Sie sind raus!
      Yes, I'm the guy responsible for the ACME cross assembler
    • Mac Bacon schrieb:

      In dem Fall wäre aber eindeutig der C-Standard Murks, denn read-sensitive-registers gibt es nicht nur in den CIAs und VDCs dieser Welt - und wenn man den Fall "ich muss einen Lesezugriff auf das I/O-Register machen, um einen Seiteneffekt auszulösen, aber der gelesene Wert interessiert mich nicht" nicht abbilden könnte, wäre C sehr kaputt.

      Der C-Standard ist da recht eindeutig: "Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine." und gibt sogar noch erläuternde Beispiele:

      "EXAMPLE 1: An implementation might define a one-to-one correspondence between abstract and actual semantics: at every sequence point, the values of the actual objects would agree with those specified by the abstract semantics. The keyword volatile would then be redundant.

      Alternatively, an implementation might perform various optimizations within each translation unit, such that the actual semantics would agree with the abstract semantics only when making function calls across translation unit boundaries. In such an implementation, at the time of each function entry and function return where the calling function and the called function are in different translation units, the values of all externally linked objects and of all objects accessible via pointers therein would agree with the abstract semantics. Furthermore, at the time of each such function entry the values of the parameters of the called function and of all objects accessible via pointers therein would agree with the abstract semantics. In this type of implementation, objects referred to by interrupt service routines activated by the signal function would require explicit specification of volatile storage, as well as other implementation-defined restrictions."

      Quellcode

      1. 10 x=rnd(-1963):fori=1to81:y=rnd(1):next
      2. 20 forj=1to5:printchr$(rnd(1)*16+70);:next
      3. 30 printint(rnd(1)*328)-217

      sd2iec Homepage
    • Ja Unseen, danke, da hatte ich offenbar eine falsche Vorstellung, was der Compiler wegwerfen darf. Allerdings wäre dann die Frage bzw der Schluss daraus: Lesen einer Speicherstelle gilt im Sinne der abstrakten Maschine als Änderung des "beobachtbaren Zustands"? Denn nur dann müsste das ja AFAIK drin bleiben, selbst wenn der gelesene Wert komplett ignoriert wird?

      enthusi: es ist offenbar der letzte Thread zum Thema. Und wenn nach Games, SID und in dem Zusammenhang Timing gefragt wird (was sicher auch kein "6. Abend" Thema ist), landet man ziemlich zwangsläufig bei volatile und der Frage, warum das falsch ist, was man an der Stelle auch ruhig verstehen darf, das schadet nix!