2 Punkte von GN⁺ 2024-04-04 | 1 Kommentare | Auf WhatsApp teilen
  • Frühe Computer mussten Funktionsaufrufe auch ohne Stack und Heap umsetzen, und Compiler verwalteten den Aufrufzustand über versteckte globale Variablen für Parameter, Rücksprungadresse und lokale Variablen
  • Der Aufrufer speicherte die Argumente, schrieb die Rückkehrposition in eine Rücksprungadressvariable und sprang dann zum Einstiegspunkt der Funktion; die Funktion sprang nach der Berechnung zur gespeicherten Adresse zurück
  • Auch logisch lokale Variablen nutzten tatsächlich globalen Speicher, sodass der Code äußerlich wie eine Funktion wirkte, intern aber eher festen Speicherbereichen und goto entsprach
  • Manche ABIs und Prozessoren optimierten Argumentübergabe und Rücksprungadressbehandlung mit Registern oder branch with link, doch die grundlegenden Einschränkungen blieben bestehen
  • Da die Rücksprungadresse derselben Funktion durch einen neuen Aufruf überschrieben wird, waren rekursive Aufrufe unmöglich; damalige Sprachen reagierten darauf, indem sie Rekursion verboten oder nur explizit erlaubten

Wie Funktionsaufrufe ohne Stack aufgebaut wurden

  • In frühen Computerumgebungen gab es weder den heute selbstverständlichen Stack noch einen Heap
  • Dynamische Speicherzuweisung ohne Heap konnte durch Puffer fester Größe ersetzt werden
    • Auch bei Daten variabler Größe wurde vorab ein ausreichend großer fester Puffer reserviert
    • Überschritten die angeforderten Daten die Pufferkapazität, beendete das Programm sich mit einem fatalen Fehler
    • Freundlichere Implementierungen erlaubten es, die maximale Kapazität zur Compile-Zeit festzulegen
    • Ausgereiftere Implementierungen legten einen eigenen Allocator über den festen Puffer und machten ihn wie allocate und free nutzbar

Aufrufkonvention auf Basis versteckter globaler Variablen

  • Um Funktionsaufrufe ohne Stack umzusetzen, definierte der Compiler für jede Funktion mehrere versteckte globale Variablen
    • Eine globale Variable je Eingabeparameter
    • Eine globale Variable für die Rücksprungadresse der Funktion
    • Globale Variablen, die lokalen Variablen entsprechen
  • Der aufrufende Code lief in folgender Reihenfolge ab
    • Er speicherte die Parameterwerte in den entsprechenden versteckten globalen Variablen
    • Er schrieb die Rückkehrposition in die Rücksprungadressvariable der Funktion
    • Er sprang per goto zur Startposition der Funktion
  • Die Funktion las und schrieb sowohl Parameter als auch lokale Variablen aus den versteckten globalen Variablen
  • Nach Abschluss legte sie den Rückgabewert in ein Rückgabewert-Register und sprang zu der Adresse, die in der Rücksprungadressvariable der Funktion gespeichert war

Beispiel: C-ähnlicher Code wird zu goto-basiertem Code

  • Die Beispielfunktion add_two_values(int a, int b) könnte ohne Stack in folgende Speicherbereiche übersetzt werden
    • a2v_a und a2v_b sind globale Variablen zum Speichern der Argumente
    • a2v_c ist eine globale Variable, die der lokalen Variable c entspricht
    • a2v_retaddr ist eine globale Variable zum Speichern der Rückkehradresse
  • Der Aufrufer sample() speichert 31415 und 2718 jeweils in den globalen Argumentvariablen
  • Anschließend schreibt er die Position resume in a2v_retaddr und springt zu add_two_values
  • add_two_values speichert das Berechnungsergebnis in return_value_register und kehrt dann über a2v_retaddr zurück
  • Der zur Position resume zurückgekehrte Aufrufer speichert den Wert aus dem Rückgabewert-Register in sample_x

Optimierung mit Registern und branch with link

  • Dieselbe Struktur ließ sich auf ABI-Ebene durch Registerübergabe beschleunigen
  • Viele Prozessoren stellten ein spezielles link register und eine branch with link-Instruktion bereit
    • branch with link speichert automatisch die Adresse der Instruktion nach der Sprunganweisung im Link Register
    • Der Aufrufer kann die ersten beiden Argumente in argument_register_1 und argument_register_2 legen
    • Die aufgerufene Funktion kann diese Registerwerte in ihre eigenen versteckten globalen Variablen übertragen und verwenden
  • Auch die Rücksprungadresse kann aus link_register in die Rücksprungadressvariable der Funktion gespeichert werden
  • Diese Optimierung behält die Grundstruktur bei, dass Aufruf und Rückkehr auch ohne Stack möglich sind

Warum Rekursion verhindert wird

  • Die zentrale Einschränkung dieser Aufrufweise ist: keine rekursiven Aufrufe
  • Bei einem rekursiven Aufruf wird die Rücksprungadressvariable derselben Funktion mit der Rücksprungadresse des neuen Aufrufs überschrieben
  • Wenn der äußere Aufruf endet, ist die ursprüngliche Rückkehrposition verschwunden, und es wird an die falsche Stelle gesprungen
  • Die damaligen Programmiersprachen umgingen dieses Problem, indem sie Rekursion nicht unterstützten
  • FORTRAN unterstützte anfangs nicht einmal Subroutinen; Subroutinen wurden 1958 hinzugefügt
  • Rekursionsunterstützung wurde in FORTRAN erst 1991 standardisiert, und auch dann mussten Subroutinen ausdrücklich mit RECURSIVE gekennzeichnet werden

Selbstmodifizierender Code und Subroutine-Instruktionen früher Prozessoren

  • Manche Compiler nutzten auf raffiniertere Weise selbstmodifizierenden Code
    • Das Adressfeld innerhalb der Sprunganweisung am Ende der Funktion diente faktisch als Rücksprungadressvariable
  • Diese Methode war nicht nur ein einfacher Trick, sondern konnte eine praktische Notwendigkeit sein
    • Manche Prozessoren unterstützten möglicherweise keine indirekten Sprünge
  • Nachdem die praktische Nützlichkeit von Subroutinen anerkannt war, fügten mehrere Prozessoren eigene Aufrufinstruktionen hinzu
    • Sie speicherten die Rücksprungadresse im ersten Wort der Subroutine
    • Die eigentliche Ausführung begann beim zweiten Wort
    • Für die Rückkehr wurde ein indirekter Sprung über das Startlabel der Subroutine ausgeführt
  • Im Beispiel-Assembly speichert bsr add_two_values die Rücksprungadresse im ersten Wort von add_two_values und beginnt die Ausführung erst bei der tatsächlichen Instruktion nach dem als Platzhalter dienenden nop

1 Kommentare

 
GN⁺ 2024-04-04
Hacker-News-Kommentare
  • Zu diesem Thema war The Art of Computer Programming wirklich großartig
    Auf den ersten Blick wirkt es altmodisch, aber es enthält unglaublich viele Algorithmen für den Umgang mit dynamisch veränderlichen Arrays oder Datenstrukturen aus der Zeit vor Heap und Stack
    Das Buch führt Schritt für Schritt bis zu Garbage Collection und der Implementierung von Lisp-Listen, und enthält genau das enzyklopädische Wissen, das man von Knuth erwartet
    Ein Beispiel, das ich besonders mag, ist die Art, wie zwei Arrays dynamisch einen gemeinsamen Speicherbereich teilen. Wenn ein Array von location#0 nach vorn wächst und das zweite Array von location#End nach hinten, wird der statisch zugewiesene Speicher effizient aufgeteilt
    Man kann das auch auf eine beliebige Anzahl von Arrays erweitern, aber an dem Punkt ist es meist besser, einfach Malloc und Realloc zu verwenden, und die Technik selbst kommt einer malloc-ähnlichen Routine schon ziemlich nahe

    • Einige Textverarbeitungen auf 8-Bit-Computern funktionierten genau so. Das Dokument belegte den gesamten verfügbaren RAM, der Text vor dem Cursor lag am Anfang des RAM, der Text nach dem Cursor am Ende des RAM
      Beim Einfügen und Einfügen aus der Zwischenablage mussten keine Daten verschoben werden, beim Navigieren hingegen schon. Trotzdem funktionierte es gut
    • Da bei den meisten Befehlssatzarchitekturen und ABIs der Stack von hohen Adressen nach unten wächst, konnte man mit dieser Technik in Single-Thread-Systemen mit wenig Speicher den Speicher zwischen Heap und Stack flexibel aufteilen
    • Die anwendungsspezifische Ressourcenzuweisung im alten MacOS lässt sich genau so erklären. Jede App hatte einen Mindestbedarf an RAM und einen bevorzugten RAM-Bedarf; beim Start belegte sie einen Slot in der bevorzugten Größe
      Wenn so viel nicht verfügbar war, wurde weniger als der bevorzugte Wert reserviert, und wenn nicht einmal das Minimum verfügbar war, schlug der Start fehl
      Soweit ich mich erinnere, platzierte das System Heap und Bibliotheken im unteren Teil dieses physischen RAM-Stücks und den Stack im oberen Teil
      Etwa mit System 8 kam eine Virtualisierungsschicht hinzu, wodurch dieser Ansatz weniger nötig wurde, und zur Zeit von MacOS X nutzte man wie andere Systeme Paging-Speicher, sodass solche Kunststücke nicht mehr erforderlich waren
      Trotzdem ist es amüsant, daran zu denken, dass so ein „ein seltsamer Trick“ aus The Art of Computer Programming einmal die Art war, wie RAM für mehrere gleichzeitig laufende Apps zugewiesen wurde
    • Interessanterweise hatte Itanium insgesamt zwei Stacks: einen für manuelles Push/Pop und einen weiteren, der durch das Register File rotierte
      Einer wuchs nach oben, der andere nach unten. Eine faszinierende Architektur, die die versprochene Leistung am Ende aber nicht lieferte
    • Auch das Festplattenformat von SQLite verwendet eine ähnliche Array-Technik, wenn es den Inhalt von Leaf-Node-Pages in Tabellen-B-Trees speichert
      Innerhalb einer Page fester Größe wächst das Offset-Array nach vorn, während das Array variabel langer Zeilenwerte vom Ende aus nach hinten wächst. So wie ich es verstehe, können beim Löschen von Zeilen im hinteren Array Löcher entstehen
      Da die Dokumentation für die B-Tree-Struktur selbst TAOCP zitiert, wäre es nicht überraschend, wenn dies eine direkte Inspiration gewesen wäre
  • Rekursive Funktionen in ALGOL aufzunehmen war ziemlich umstritten und ist bis heute eine interessante Geschichte: https://vanemden.wordpress.com/2014/06/18/how-recursion-got-...

  • Forth-Interpreter für die SUBLEQ-Maschine (https://github.com/howerj/subleq) und einen Interpreter für eine bitserielle Maschine (https://github.com/howerj/bit-serial) habe ich geschrieben; beide hatten keinen für Forth nötigen Function-Call-Stack
    SUBLEQ erlaubt nicht einmal indirektes Laden/Speichern, sodass man für alles auch nur halbwegs Komplexe selbstmodifizierenden Code braucht
    Mein Ansatz war, für beide Maschinen eine virtuelle Maschine zu bauen, die solche Funktionen übernehmen kann, und auch kooperatives Multithreading einzubauen
    Wenn man einen Heap braucht, schreibt man ihn in Forth; auch das Floating-Point-Word-Set schreibt man in Forth. Viele MCUs haben immer noch keine Floating-Point-Instruktionen, und man kann das über Software-Funktionsaufrufe implementieren
    Andere Compiler wurden zwar nicht erwähnt, haben aber vermutlich ähnlich gearbeitet. Einige BASIC-Interpreter implementierten ebenfalls erst eine VM und zielten dann darauf; P-Code ist ähnlich

    • Der TI-99/4A hatte nur 256 Byte, also 128 Wörter, an Haupt-RAM, auf das die CPU direkt zugreifen konnte
      Der Großteil des Basissystemspeichers war Video-RAM und musste über eine ziemlich umständliche Prozedur durch poke/peek auf die Register des Videochips angesprochen werden
      Der Videochip hielt einen automatisch inkrementierenden aktuellen Speicherzeiger vor, sodass der Zeiger bei fortlaufendem Lesen oder Schreiben jeweils um 1 erhöht wurde; allein die Tatsache, dass der Großteil des Systemspeichers nur auf diese Weise erreichbar war, machte das Schreiben großer Programme aber schwierig
      Deshalb baute TI eine abstrakte Maschine namens GPL, die diesen Zugriff auf Video-RAM natürlicher machte. Da sie jedoch auf dem TMS9900 interpretiert ausgeführt wurde, war sie langsamer als nativer Code; außerdem konnte die CPU nur dann auf das RAM des Videochips zugreifen, wenn der Chip gerade kein Bildschirm-Scanout machte, etwa während der horizontalen/vertikalen Austastlücken, was es noch langsamer machte
      BASIC-Code und Variablen lagen ebenfalls vollständig in diesem Videospeicher, daher ist ziemlich offensichtlich, womit der BASIC-Interpreter des TI-99/4A geschrieben war. Schnell war er ganz und gar nicht
      Interessant ist, dass der TMS9900 keine echten Allzweckregister hatte. Die Workspace-Register WR0 bis WR15 lagen irgendwo im Speicher, und das WP-Workspace-Pointer-Register zeigte darauf
      Die physischen Register der CPU waren nur PC, WP und das Statusregister. Dadurch war eine sehr primitive Form von Register Windowing möglich: Verzweigte man mit der Instruktion BLWP, wurde ein neuer Satz von „Registern“ an einer anderen Speicherstelle aktiv, und die Rücksprungadresse wurde im neuen Workspace gespeichert
      Dass ich in letzter Zeit oft über den TI-99/4A spreche, liegt daran, dass ich als persönliches Projekt einen Assembler für diese Maschine baue
    • Beim Lernen und Vertiefen von Forth und Subleq bin ich auf diese Arbeiten gestoßen. Es war schön, den Ansatz zu lesen, und ich wollte das Buch kaufen, aber Amazon sagt, das gehe nicht. Ich frage mich, ob es einen Nachdruck geben wird
    • Ich wollte Subleq erwähnen; dort ist es wirklich schwer, auch nur „Hello world“ auszugeben
  • Die Aussage, dass einige Prozessoren die Rücksprungadresse im Wort direkt vor der ersten Instruktion der Subroutine speicherten, stimmt; der PDP-8 tat das
    Die Evolution des PDP-8 lässt sich auch als Weg hin zu Hardware-Unterstützung für Rekursion betrachten
    Anfangs schrieb die JMS-Instruktion die Rücksprungadresse in das erste Wort der Funktion. Oft legte der Aufrufer die Argumente hinter die JMS-Instruktion, und der Aufgerufene las die Argumente über Offsets relativ zur Return-Instruktion und inkrementierte sie dabei jedes Mal, sodass die Rücksprungadresse wieder auf die Codeposition zeigte
    Später wurde es recht üblich, mit einer der Auto-Increment-Positionen einen einfachen Stack zu bauen. Der PDP-8 hatte acht Speicherstellen, die sich bei jeder Verwendung als Pointer inkrementierten, und Funktions-Prolog/Epilog verwalteten diesen Stack selbst, was vollständige Rekursion ermöglichte
    Noch später bekamen Mikroprozessor-Implementierungen wie der Harris 6120 einen Hardware-Stack, was die Performance verbesserte

    • Der Librascope LGP-30 von 1956 hatte eine R-Instruktion, also eine Instruktion zum Speichern der Rücksprungadresse
      Diese Instruktion speicherte das bereits inkrementierte PC+1 im Adressteil der Instruktion an der Zielposition; per Konvention war dieses Ziel die unbedingte Sprunginstruktion direkt vor Beginn der Subroutine
      Nach der R-Instruktion stand eine U-Instruktion für den unbedingten Sprung zu der betreffenden Subroutine
      Die Subroutine kehrte zurück, indem sie an die Adresse vor sich verzweigte; dort stand ein unbedingter Sprung zurück direkt hinter die Aufrufstelle
      Ohne fortgeschrittenere Calling Conventions war Rekursion unmöglich. Und alle Instruktionscodes der Assemblersprache bestanden aus einem einzigen Buchstaben
    • IBM 1800, IBM 1130 und viele Maschinen aus dieser Zeit machten es genauso. Maschinen mit ausreichend Registern, etwa die Xerox-Sigma-Reihe, konnten diese Praxis vermeiden
  • Bei Programmen für AVR-8 fühlt sich die Verwendung der C-Calling-Convention manchmal verrückt an
    In Assembler kann man Variablen innerer Schleifen dauerhaft in der großen Registerdatei halten, oder man kann die im Artikel beschriebenen Methoden verwenden
    Für solche Apps ist auch die Methode, Funktionen „einzufärben“, gut. Wenn man weiß, dass eine rote Funktion und eine grüne Funktion nie gleichzeitig aktiv sind, kann man lokale Variablen oder Parameter der beiden wiederverwenden

    • Beim Arbeiten in eingeschränkten Umgebungen kann der Stack-Verbrauch von C unintuitiv sein, besonders wenn man an die Bequemlichkeiten von Desktop-Betriebssystemen gewöhnt ist
      In einem Mikrocontroller-Codebase-Projekt, zu dem ich früher dazustieß, verfolgten mehrere Entwickler seit Wochen schwer zu fassende Bugs in mehreren Subsystemen
      Wenn man Code verschob, wanderten die Bugs mit. Nach etwas Nachverfolgung und dem Setzen von Fallen konnte ich Code-Stellen finden, an denen der Call-Stack zu tief wurde und andere Datenstrukturen überschrieb
  • Als ich Programmieren lernte, wurde ich genau zu dieser Art des Programmierens gezwungen. Nicht in den 1970ern, sondern 2001
    Denn meine erste Programmiererfahrung war die halb grafische Skripting-„Sprache“ des Spieleentwicklungstools RPG Maker 2000
    Wenn ihr RM2K-Skripting noch nie gesehen habt, stellt euch eine Mischung aus Scratch und Emacs’ Paredit-Modus vor. Beispiel: https://forums.rpgmakerweb.com/data/attachments/21/21958-f89...
    Es sieht aus wie Text, lässt sich aber nicht wie Text bearbeiten, sondern nur als Blöcke mit zugehörigen Eigenschaftsdialogen
    Natürlich hatte die Skriptsprache des RPG Maker auch nichts Schickes wie einen Stack. Wenn man eine wiederverwendbare Subroutine brauchte, musste man geheime globale Variablen für die Parameter reservieren; Reentrancy gab es nicht
    Rückblickend hätte man wohl, wenn man nur stur genug gewesen wäre, sowohl Register als auch einen Runtime-Stack im RPG Maker 2000 implementieren können
    Anfangs sieht es einfach aus. Man kann Fake-„Register“ wie die Zero Page des 6502 bauen, und mit indirektem Variablenzugriff (https://rpgmaker.net/tutorials/523/) auch einen Stack
    Das Problem ist, dass RM2K Nebenläufigkeit in Form von „Parallel Process“-Skripten hat. Wenn parallele Prozesse solche Abstraktionen verwenden, überschreiben verschiedene „Threads“ munter gegenseitig ihren Zustand
    Daher bräuchte man für jeden „virtuellen Core“ mehrere Zero Pages und Stacks und müsste jedem parallelen Skript einen virtuellen Core zuweisen/an ihn binden/ihn schedulen. Mit anderen Worten: Jedes Skript müsste irgendwie einen nur ihm bekannten Stack Pointer haben
    Um das auch bei Race Conditions robust zu machen, bräuchte man normalerweise so etwas wie einen Mutex
    Wenn man bedenkt, wie hartnäckig RPG-Maker-Spieleentwickler sind, hat wahrscheinlich irgendwer einen Weg gefunden, irgendeine Runtime-Funktion so zu missbrauchen, dass sie sich wie ein Mutex verhält; aber ehrlich gesagt macht mir der Gedanke Angst, wie das in der Praxis aussah

    • Ich habe auch mit rpgmaker angefangen, und diese Geschichte macht mich wirklich nostalgisch
      Ich erinnere mich, wie ich auf rpgmaker.net ein Spiel mit einem custom battle system heruntergeladen habe. Das war eine Implementierung, die das gesamte eingebaute Kampfsystem mit Techniken wie den von dir beschriebenen ersetzt hatte
      Als ich es im Editor geöffnet und mir angesehen habe, wie es funktionierte, war ich völlig überwältigt. Es gab Hunderte von „Variablen“, und wenn ich mich richtig erinnere, waren nur i64 erlaubt, dazu Hunderte von „Switches“. Die Switches waren Boolean-Werte
      Damals hatte ich keinerlei Vorstellung von Stack, Heap oder Funktionsaufrufen
      Ich kann mir nicht einmal vorstellen, wie viel Energie es gekostet haben muss, das zu bauen und zu warten/debuggen
  • Wenn ich mich richtig erinnere, schrieb man BASIC-Programme auf dem ZX81 ziemlich nah an „ohne Stack“
    1 GOTO 30
    10 LET C = A + B
    20 RETURN
    30 LET A = 1
    40 LET B = 2
    50 GOSUB 10
    60 LET A = C
    70 LET B = 3
    80 GOSUB 10
    90 PRINT C
    RUN
    6
    Im Grunde machte ich selbst das, was im Artikel der Compiler erledigt. Zeilennummern waren Speicheradressen, und die versteckten Variablen waren vor mir nicht versteckt. Denn ich war der Compiler
    Das Einzige, was der Interpreter für mich tat, war, die Rücksprungadresse von GOSUB zu speichern
    Allerdings kann der Code syntaktisch falsch sein oder meine Erinnerung verzerrt. 40 Jahre sind eine lange Zeit, aber die Grundidee stimmt
    Außerdem hatte der Z80-Prozessor in der Maschine Stack-Management-Funktionen. Der BASIC-Interpreter war wirklich einfach, aber dafür gab es Entschuldigungen: nur 1 KB RAM und 8 KB ROM für OS, Interpreter und alles andere

    • Auch das verwendet zumindest einen Call Stack. GOSUB speichert die Zeilennummer oder eine andere Referenz, auf die RETURN zugreift, und wenn man GOSUB-Aufrufe verschachtelt, muss es mehrere Rücksprungpunkte merken, also braucht es in irgendeiner Form einen Stack
      Manche BASICs hatten allerdings statt eines allgemeinen Stacks nur ein festes Array von Return-Pointern und einen Index auf die aktuelle Position, sodass die Aufruftiefe zum Beispiel fest auf 7 begrenzt war. Aus Sicht des Programmierers verhielt sich das wie ein Call Stack
      Natürlich ist das kein „richtiger“ Stack mit lokalen Variablen/Parametern, wie man ihn erwarten würde, wenn jemand Stack sagt
      In der Standardumgebung von BBC BASIC konnte man eine interessante Demo zeigen, was bei verschachtelten Aufrufen, einschließlich Rekursion, passiert. Wenn man die Stack-Position ganz oben im Display-Speicher platzierte und dort nichts zeichnen ließ, konnte man sehen, wie der Stack während der Arbeit wuchs
      Wegen der niedrigen Bildschirmauflösung waren die 2 Bytes einer Rücksprungadresse in Bildschirmmodus 1 oder 5 als acht dicke Pixel zu sehen. In Modus 2 waren es vier, aber mit blinkenden Farben war das weniger gut; in Modus 0, 3, 4 und 6 waren es 16, aber bitweise war es schwerer zu erkennen als wiederholte acht Farben
  • Bevor es beliebig erweiterbare Heaps gab, mussten Programmierer zumindest ein wenig Engineering-Urteil aufbringen
    Denn sie mussten die probabilistische Verteilung der Eingaben berücksichtigen und alle Zwischenspeicher angemessen dimensionieren
    Daher gab es dann „BUGS AND LIMITATIONS“

    • Solche alten Methoden sind je nach Aufgabe auch heute noch aktuell. In harter Echtzeit verwendet man kaum dynamischen Speicher, vor allem weil die Zeiten für Speicherallokation und -freigabe nicht deterministisch sind
      Deshalb wird alles zur Compile-Zeit statisch allokiert, und man muss wissen, wie viel Speicher die Eingaben verbrauchen werden
      Aber die obere Grenze des Speicherverbrauchs zu kennen, war früher auch für Anwendungsprogrammierer normal. Schließlich will man niemals Out-of-Memory haben
      Heutzutage lässt man die Speichernutzung wohl einfach YOLO laufen
    • Historisch war auch eines der großen Ziele von GNU genau so etwas: künstliche Beschränkungen in den Kern-Utilities zu beseitigen
      Das war zum Beispiel eine große Verbesserung gegenüber Einschränkungen wie einer endlichen und kurzen Maximallänge für sed-Befehle
    • Der eigentliche Fehler war, Menschen Eingaben für Computerprogramme liefern zu lassen
  • Ich habe zu lange funktional programmiert, sodass es mir wirklich schwerfällt, mir vorzustellen, wie man Code ohne Rekursion schreibt
    Technisch weiß ich, wie man rekursive Algorithmen in iterative umwandelt, und ich habe das auch schon dort gemacht, wo die Ressourcen stark begrenzt waren, aber mögen tue ich es nicht
    Meist ist die rekursive Variante schöner, und in 99 % der Fälle halte ich sie für schnell genug. Wenn der Compiler Tail Recursion unterstützt, nähert sich das 100 %, aber bei den meisten interessanteren Aufgaben muss man den Stack ohnehin selbst verwalten
    Manchmal mache ich solche Dinge absichtlich, um zu lernen, wie man das vor meiner Geburt gemacht hat. Ich bastle gelegentlich an Commodore-64-Spielen herum, und dabei wird mir sehr deutlich, was für ein Luxus es ist, heute an schnelle, billige und leicht zu benutzende Hardware gewöhnt zu sein

    • Heutige Instruction Sets sind eindeutig viel nützlicher
      Um auf solchen alten Maschinen Rekursion zu verwenden, musste man seinen eigenen Stack-Mechanismus bauen, und selbst dann blieb noch das Problem, dass man außer globalem Speicher im Grunde keine andere Möglichkeit hatte
      Ich habe diese Zeit erlebt, würde sie aber niemandem empfehlen
  • Bei der @let-Funktion von Enhanced GNU Awk ließ man den Compiler für @let-Blöcke außerhalb von Funktionen, etwa in BEGIN- oder END-Blöcken, geheime globale Variablen anlegen
    Diese Variablen werden zwischen Blöcken so weit wie möglich wiederverwendet
    $ ./gawk --dump-variables 'BEGIN { @let (a, b, c = 1) { } }'
    $ cat awkvars.out
    $let0001: untyped variable
    $let0002: untyped variable
    $let0003: 1
    ARGC: 1
    ARGIND: 0
    ARGV: array, 1 elements
    BINMODE: 0
    [ .. snip many ]
    https://www.kylheku.com/cgit/egawk/about/

    • Diese Website funktioniert bei meinem ISP nicht. ping geht nicht, und nc -z 104.37.63.7 443 auch nicht
      Update: Offenbar ist die Sicherheitsinfrastruktur kaputt. Ich weiß nicht einmal, was das ist, und benutze auch kein Twitter. Wenn man das AS prüft, ist es Google Fiber
      Und bitte doxxt mich nicht