Hello, Guest the thread was called8.7k times and contains 86 replays

last post from JeeK at the

Wer hat Lust ein Text-Adventure zu programmieren? (C64 Basic 2.0)

  • Dachte ich auch, bis ich in C# auf die Nase gefallen bin und gelernt habe, dass man für exzessives Stringhandling besser den StringBuilder verwendet. ;)

    Hier OT, aber das muss doch ne recht alte Version gewesen sein. Soweit ich weiß optimiert der Compiler da ganz gut und setzt den StringBuilder automatisch ein wo es sinnvoll ist. Aber selbst wenn nicht, ich wette das Problem ist dann nicht die GC sondern schlicht der Overhead beim Erzeugen der ganzen Objekte, weil "string" in .NET nunmal immutable ist -- malloc() und dessen Verwendung von Betriebssystemfunktionen (sbrk(), mmap() -- oder was Windows da eben bietet) lassen grüßen ;)

    Klar, genauso wie immer: Am Basic vorbei die Kernalfunktionen direkt mit SYS aufrufen. Aber echtes/reines "BASIC V2" und "vernünftig" schließt sich halt aus.

    Dachte ich es mir doch, das war die lange Version von "nein" ;) Fühle mich nur wieder darin bestätigt, der Sprache, mit der ich seinerzeit als Kind programmieren gelernt habe, den Rücken gekehrt zu haben ....

  • Soweit ich weiß optimiert der Compiler da ganz gut und setzt den StringBuilder automatisch ein wo es sinnvoll ist.


    Dachte ich es mir doch, das war die lange Version von "nein" ;) Fühle mich nur wieder darin bestätigt, der Sprache, mit der ich seinerzeit als Kind programmieren gelernt habe, den Rücken gekehrt zu haben ....

    Klar, und die Kernal-Funktion machen dann genau das, was der entsprechende Basic-Befehl auch getan hätte (der ruft nämlich auch die Kernelfunktion auf). :platsch:


    Da ihr hier nicht mit belastbaren Beispielen rüberkommt, steige ich mal aus. Ist ja auch alles gesagt.

  • Witzig. Ich habe früher einiges an CBM und C64 Software in Basic fabriziert und (fast) nie war die Garbage Collection ein Problem.
    Die schlägt nur zu, wenn man wie blöd ständig neue Strings anlegt. Das kann man aber in der Regel vermeiden. In einem Adventure, wo die meisten Texte konstant sind, wird das Problem bei sinnvoller Programmierung gar nicht auftreten.
    Aber neuerdings scheinen alle der Meinung zu sein, dass das Commodore auf Grund der GC völlig unbrauchbar ist. :D

    Verwaltungsprogramme, die die Datensätze in Strings im RAM halten (der Geschwindigkeit wegen), leiden naturgemäß unter der Laufzeit der GC, weil bei 600 Strings die je einen Datensatz darstellen (mit fixen Positionen für die Felder, weil man die sonst nicht schnell adressieren kann), wobei dann abzüglich des Programms selbst relativ wenig freier Speicher übrig bleibt. Damit erhöht sich die Wahrscheinlichkeit der GC-Auslösung von der Füllrate des freien Speichers. Schon alleine beim Laden und Aufbereiten bzw. Formatieren der Datensätze, oder Sortieren, Eingeben etc. fällt genügend Stringmüll an. Bei einer Anzahl von 600 und der schon erwähnten quadratischen Laufzeit, sind die Wartezeiten ohne ordentlicher GC inakzeptabel. Selbst dann, als das Sortieren in Assembler mit Descripter-Tausch implementiert war, gab es noch viele andere Stellen, an dem Stringmüll entsteht.


    Es gibt in der Tat viele Programme, die kein Problem mit der GC haben, sei es durch die geringe String-Anzahl oder viel freiem Platz, oder der geringen Rate, mit der der String-Müll entsteht. Aber es gibt auch immer wieder solche, wo sie sich nicht vermeiden lässt, unschöne, deutliche Verzögerungen zu verursachen. ;)

  • verstehe ich das richtig? a$ wird nich recycled?

    Wird mitunter recycled. Es gibt beim Anfordern von neuem Speicher am Stringheap genau einen Fall, nämlich wenn der gerade "verworfene" String quasi zuoberst am Heap liegt, dann wird dieser verworfen und dann erst neu angelegt. :D
    Im GET A$ Fall allerdings nicht, weil der Zusammenhang zwischen verworfenem String und neuem String für obige Optimierung nicht gegeben ist.
    Daher frisst bei gedrückter Taste jeder GET-Aufruf ein Byte mehr vom String-Heap. X/

  • Garbage-Collection.Kann man da nicht einfach regelmäßig free(0) aufruhfen?

    Nein, das ist eine Urban Legend, die einfach nicht aus der Welt zu schaffen ist. Man kann damit nur gezielt den Zeitpunkt bestimmen, ab wann der maximal freie Speicher wieder vorliegt. An der Laufzeit der GC ändert das aber nichts. Über die gesamte Programmlaufzeit betrachtet, bewirkt man mit so "vorgezogenen" GC-Aufrufen in Summe eine längere GC-Laufzeitanteil. Wenn der Zeitpunkt ein Rolle spielt, mag das legitim sein, aber sonst ist man mit den Verbrauchenlassen des freien Speichers bis zum Ende beim längsten Intervall des GC-Auftretens ...

  • Tja, "zuschlagende" GC ist ja sowas von 1985, zum Glück ist das in modernen Sprachen alles viel besser gelöst. Hatte mal einen Kollegen, den einzigen mit dem ich je hart aneinandergeraten bin (aber das aus nochmal einem anderen absurden Grund), der hielt es in .NET für eine gute idee, die GC direkt anzuprogrammieren. Wir sind ihn losgeworden, das war gut so Egal, ich schweife ab....

    Es gibt auch im modernen Sprachen eine GC - es heißt nicht automatisch, dass eine GC langsam sein muss. Das ist alles nur eine Frage der Datenstrukturen und Algorithmen. Aber auch das "alte" BASIC der 3000/4000er PETs hatte mit BASIC 4.0 bereits die GC in Backlink-Implementierung mit linearer Laufzeit. Dass man für den VIC-20 das "schmale" BASIC V2 wird auch wohl mit dem knappen Speicher im ROM zu tun gehabt haben. Bei 3,5 KByte RAM hat man das womöglich als ungefährlich eingeschätzt. Außerdem hätte die bessere Backlink-Implementierung jeden nichtleeren String am Heap fix 2 Bytes gekostet.
    Sobald Banking und mehr ROM-Speicher im Spiel war, war auch seit BASIC 3.5 (C16/116/Plus4) und dann in BASIC 7.0 (C128) die Backlink-Implementierung der Standard.

  • Klar, und die Kernal-Funktion machen dann genau das, was der entsprechende Basic-Befehl auch getan hätte (der ruft nämlich auch die Kernelfunktion auf). :platsch:
    Da ihr hier nicht mit belastbaren Beispielen rüberkommt, steige ich mal aus. Ist ja auch alles gesagt

    Das hättest du besser tun sollen, bevor du diesen Unsinn raushaust. KERNAL Routinen werden niemals BASIC -Variablen oder -Strings anrühren, sowas kennt der KERNAL gar nicht. Die KERNAL Routine zur Eingabe gibt das Zeichen in A (Register) zurück -- da käme man sogar tatsächlich von BASIC aus ran, weil SYS die Register vor dem Rücksprung ins BASIC Programm sichert.

    Nein, das ist eine Urban Legend, die einfach nicht aus der Welt zu schaffen ist. Man kann damit nur gezielt den Zeitpunkt bestimmen, [...]

    Und genau darum geht es Leuten, die das anwenden. Nicht die Gesamtlaufzeit soll besser werden sondern es sollen kleinere Runs stattfinden, am besten zu weniger auffälligen Zeitpunkten. Also, keine Legende.

    Es gibt auch im modernen Sprachen eine GC - es heißt nicht automatisch, dass eine GC langsam sein muss.

    Nichts anderes habe ich behauptet. Aber es geht dabei nicht einmal nur um die Implementierung der GC an sich, sondern auch um die Strategie, wann gesammelt wird. Warten bis wirklich kein Speicher mehr da ist das macht heute keine GC mehr.

  • Klar, genauso wie immer: Am Basic vorbei die Kernalfunktionen direkt mit SYS aufrufen. Aber echtes/reines "BASIC V2" und "vernünftig" schließt sich halt aus.

    Musste ich jetzt mal kurz austesten, das sieht sogar am Ende noch ganz lesbar aus


    Code
    1. 10 sys65508:c=peek(780):if c=0 then 10
    2. 20 if (c>47) and (c<58) then print chr$(c);
    3. 30 if (c>64) and (c<91) then print chr$(c);
    4. 40 if c <> 13 then 10

    Wenn man nur mit c arbeitet (z.B. für ein Menü) spart man sich in der Tat jeden String. Ist dann aber irgendwie kein BASIC mehr ;)


    Falls jemand noch eine Erklärung zum Problem hier mag, die Kernal Funktion, die hier aufgerufen wird, sieht in Pseudocode ungefähr so aus


    Code
    1. char GETIN() {
    2. Device dev = getCurrentDevice();
    3. return dev.readChar();
    4. }

    Nun kennt BASIC überhaupt keinen Datentyp für ein einzelnes Zeichen -- und anstatt wie im Code oben einfach eine Zahl zu nehmen, macht das BASIC GET etwa sowas:


    Code
    1. GET(ref str)
    2. {
    3. char c = GETIN();
    4. str = new String(c);
    5. }
  • Also wie ist das nun? Wenn man 10 GET A$: IF A$="" THEN 10 macht, wird dann pro Durchlauf ein neuer String angelegt? Oder wird der A$ jedesmal "wiederverwendet"? Dann wuerde ja der Speicher innerhalb kurzer Zeit voll sein, wenn der Benutzer ein paar Sekunden lang keine Taste drueckt?

  • Oder einfach z.B. mit der Super Garbage-Collection ... (seit 1985 bewährt und erprobt). :D
    Falls der Bereich $E000 bis $F000 für eine Hires-Grafik braucht, muss man den Puffer der GC woanders hinlegen oder/oder verkleinern.


    Weißt du noch ungefähr, welche Ausgabe des Sonderheftes das gewesen sein könnte?

    Die Suche war nicht ganz einfach, meine Erinnerung hat mir da einen Streich gespielt. Ich dachte, es sei 64'er Sonderheft 02/1985 ("Abenteuerspiele selbst programmiert") gewesen, aber es war eine normale 64'er 02/1986, S. 53, "Weg mit dem Müll".


    Aber wie ich sehe, bezieht sich die Super Garbage-Collection auch auf diesen Artikel mit der Bemerkung "fehlerhaft in der Implementierung".

  • Und genau darum geht es Leuten, die das anwenden. Nicht die Gesamtlaufzeit soll besser werden sondern es sollen kleinere Runs stattfinden, am besten zu weniger auffälligen Zeitpunkten. Also, keine Legende.

    Doch, denn es gibt keine "kleineren" Runs. Man kann sie nur vorziehen. Die GC hängt von der Anzahl der Strings und nicht von der Müllmenge ab!
    Das führt zu dem Denkfehler, das geglaubt wird, häufige GC-Aufrufe ließen die GC schneller ablaufen. Mitnichten! In Summe verbringt der Rechner lediglich
    noch mehr Zeit in der der GC ...
    Ich sag es noch mal, man den Zeitpunkt, wann die Störung durch die GC auftritt, insofern steuern (vorziehen), und mit einer gewissen Wahrscheinlichkeit (abhängig davon, wie viel freier Platz geschaffen wurde) sagen oder hoffen, dass man eine gewisse Zeit ungestört Strings "erzeugen" kann. Mehr nicht. ;)

  • Also wie ist das nun? Wenn man 10 GET A$: IF A$="" THEN 10 macht, wird dann pro Durchlauf ein neuer String angelegt? Oder wird der A$ jedesmal "wiederverwendet"? Dann wuerde ja der Speicher innerhalb kurzer Zeit voll sein, wenn der Benutzer ein paar Sekunden lang keine Taste drueckt?

    Die Variable A$ hat ja nur den Stringdescriptor bestehend aus Länge und Adresse auf den String. Solange die Eingabe leer bleibt, gibt es auch keinen neuen String, der Descriptor hat als Länge 0 und die Adresse ist nicht definiert (bzw. irrelevant). Erst wenn tatsächlich eine Taste gedrückt wird, wird ein String am String-Heap angelegt und im Descriptor (der in der Variable steckt) wird die neue Adresse eingetragen. Kann man mit folgendem Programm, der den aktuellen Heap-Pointer zeigt, beobachten. :D

    Code
    1. 10 PRINT "{HOME}"PEEK(51)+PEEK(52)*256
    2. 20 GETA$:IFA$<>""THEN 10
    3. 30 PRINTA$
    4. 40 GOTO 10

    D.h. mit jeder gedrückten Taste (wenn GET gerade abfragt) wird ein String angelegt.

  • Also wie ist das nun? Wenn man 10 GET A$: IF A$="" THEN 10 macht, wird dann pro Durchlauf ein neuer String angelegt?

    Nicht pro Durchlauf, nur pro gedrückter Taste. Wenn keine Taste gedrückt wird, wird ja ein Leerstring erzeugt, und dieser benötigt keinen Speicher im Stringstapel (die Längeninformation, also hier Null, steht direkt in der Variablenstruktur für A$, der Zeiger bleibt auf seinem vorigen Wert, da er bei Länge Null ja eh ignoriert wird).

    Dann wuerde ja der Speicher innerhalb kurzer Zeit voll sein, wenn der Benutzer ein paar Sekunden lang keine Taste drueckt?

    Probier mal in VICE:

    Code
    1. 10 GET A$:GOTO 10
    2. RUN

    Nun tipp die Buchstaben von a bis z und sieh Dir im Monitor den Speicher von $9f00 bis $a000 an - alles schön sauber geloggt. :D
    Zweites Beispiel:

    Code
    1. 10 A$=CHR$(234):GOTO 10
    2. RUN

    Im Monitor kann man dann mitverfolgen, wie der Speicher rückwärts mit NOPs gefülllt wird.

  • Die Suche war nicht ganz einfach, meine Erinnerung hat mir da einen Streich gespielt. Ich dachte, es sei 64'er Sonderheft 02/1985 ("Abenteuerspiele selbst programmiert") gewesen, aber es war eine normale 64'er 02/1986, S. 53, "Weg mit dem Müll".


    Aber wie ich sehe, bezieht sich die Super Garbage-Collection auch auf diesen Artikel mit der Bemerkung "fehlerhaft in der Implementierung".

    Ah, danke für die Recherche. Das ist ja dann eh ein alter Bekannter.
    In der Tat, die Bugs sind echt ernst:

    • Der Stringdescriptor-Stack wird nicht berücksichtigt (wenn die GC in einer Ausdrucksauswertung passiert, ist der temporäre String kaputt).
    • Der Pufferbereich kann überlaufen - Daten außerhalb der Puffergrenzen werden überschrieben (unterhalb $A000 in den Heap hineingehend)!
    • String-Selektion für den Übertrag in den Puffer ist fehlerhaft.

    Nicht unbedingt ein Bug, aber das Handling von ROM/RAM und Interrupts ist umständlich und ineffizient.
    Die Bugs bewirken, dass tatsächlich Daten zerstört werden und String-Inhalte einfach nicht mehr stimmen. In einfachen Fällen, die der Autor wohl getestet hat, funktioniert es nämlich. In einem Stresstest (auch in normalen Programmen können diese Situationen entstehen) kommt das zu Tage, d.h. auf diese Implementierung kann man sich nicht verlassen ... :/


    Ich hab auch eine Bug-vereinigte und optimierte Version von Garbage64, wie die obige Implementierung heißt, weil ich eine funktionierende Variante für meine Messungen und Testprogramme brauchte, nur um diverse Ansätze zu vergleichen. ^^

  • Nein, das ist eine Urban Legend, die einfach nicht aus der Welt zu schaffen ist. Man kann damit nur gezielt den Zeitpunkt bestimmen, ab wann der maximal freie Speicher wieder vorliegt. An der Laufzeit der GC ändert das aber nichts. Über die gesamte Programmlaufzeit betrachtet, bewirkt man mit so "vorgezogenen" GC-Aufrufen in Summe eine längere GC-Laufzeitanteil. Wenn der Zeitpunkt ein Rolle spielt, mag das legitim sein, aber sonst ist man mit den Verbrauchenlassen des freien Speichers bis zum Ende beim längsten Intervall des GC-Auftretens ...

    Äh ... nee

  • Bei mir war seinerzeit eine Eingaberoutine wie folgt schuld, dass der Stringspeicher zuwuchs:


    Code
    1. 10 DIM A$(12000)
    2. 20 PRINT"FREIER SPEICHER:";(PEEK(51)-PEEK(49)+256*(PEEK(52)-PEEK(50)))
    3. 30 S$=""
    4. 40 GETB$:IFB$=""THEN40
    5. 50 PRINTB$;
    6. 60 IFB$=CHR$(13)THEN80
    7. 70 S$=S$+B$:GOTO30
    8. 80 PRINT"EINGABE WAR "S$
    9. 90 GOTO 20

    Die Eingaberoutine ist der Teil von Zeile 20 bis 70, ich hatte im ursprünglichen Code noch die Steuerzeichen herausgefiltert, sodass die Eingaberoutine "cooler" als INPUT ist.
    Das erzeugt jedenfalls ordentlich temporäre Strings, Zeile 20 gibt das nach jeder Eingabe aus. FRE() habe ich hier bewusst nicht verwendet denn dass hätte jedesmal eine Garbage Collection getriggert. Zeile 10 ist übrigens nur zum Speicherplazverbrauchen da, damit nur wenig mehr als 2K überbleiben.
    Nach und nach werden die 2K weniger, aber wenn man lange genug herumtippt kommt es zu einer automatichen GC, die aufgrund des kleinen Stringspeicherbereichs nicht viel zum aufräumen hat.


    Langer Rede kurzer Sinn:
    1. Aufpassen mit derartigen Eingaberoutinen, Formel in Zeile 20 hilft, Speicherfresser zu entdecken
    2. ansonsten spricht meiner Meinung nach generell nichts gegen ein Textadventure in BASIC :-)

  • Weil's gut dazupasst, habe neulich diese Serie von Anleitungen gelesen:


    Lets make a Commodore 64 Text Adventure Game! von John Christian Lonningdal


    Ist auf Englisch, aber sehr gut aufgebaut und enthält neben den programmiertechnischen Dingen auch einige Tipps wie man ein gutes Adventure macht. Der Scott Adams der da erwähnt wird ist übrigens der Dilbert-Zeichner, wusste gar nicht dass der mal für den C64 programmiert hat.