====== Lyx – Memory Management ====== Lyx hat keinen Garbage Collector. Das ist keine Einschränkung — es ist ein Designziel. GC-Pausen sind nicht-deterministisch: Sie können jederzeit einsetzen, beliebig lange dauern und Echtzeitgarantien zerstören. In eingebetteten Systemen, Flugsteuerungen und Hochlast-Servern ist vorhersagbares Speicherverhalten keine Option, sondern Pflicht. Lyx gibt dem Entwickler vollständige Kontrolle über drei Speicherbereiche: ^ Bereich ^ Allokation ^ Freigabe ^ Einsatz ^ | **Stack** | Automatisch bei Variablendeklaration | Automatisch bei Scope-Ende | Lokale Variablen, feste Arrays, Structs | | **Heap** | Explizit mit ''new'' / ''malloc'' | Explizit mit ''dispose'' / ''free_mem'' | Daten die Scope-Grenzen überschreiten | | **Statischer Speicher** | Zur Compile-Zeit | Nie | Konstanten, String-Literale, globale Puffer | ---- ===== 1. Stack-Allokation ===== Der Stack ist der schnellste und sicherste Speicherbereich. Jede lokale Variable, jeder feste Array und jeder Struct liegt automatisch auf dem Stack — kein Allokationsaufruf, keine Freigabe. fn ProcessData(): void { var counter: int64 := 0; // 8 Byte auf dem Stack var ratio: f64 := 1.5; // 8 Byte auf dem Stack var name: pchar := "Sensor A"; // Zeiger (8 Byte) auf statischen String var readings: f64[64]; // 64 × 8 = 512 Byte auf dem Stack var header: PacketHeader; // Struct — Größe zur Compile-Zeit bekannt // counter, ratio, readings, header werden automatisch freigegeben // wenn ProcessData() zurückkehrt } **Vorteile des Stacks:** * Allokation = Stapelzeiger-Verschiebung — eine einzige Assembler-Instruktion * Keine Fragmentierung * Deterministische Freigabe am Scope-Ende * WCET-berechenbar — der Speicherverbrauch ist statisch bekannt **Einschränkung:** Die Größe muss zur Compile-Zeit bekannt sein. ''var buf: uint8[n]'' mit einem Laufzeit-Wert ''n'' ist nicht zulässig. ==== Stack-Tiefe und Scope ==== Lokale Variablen leben genau so lange wie ihr Scope. Beim Verlassen des inneren Blocks wird der Speicher sofort freigegeben: fn Example(): int64 { var outer: int64 := 10; { var inner: int64 := 20; // lebt nur in diesem Block outer := outer + inner; } // inner ist hier nicht mehr zugänglich — Speicher freigegeben return outer; // 30 } ---- ===== 2. Stack-Limit und statische Stack-Analyse ===== Stack-Overflows sind in eingebetteten Systemen eine der häufigsten Fehlerquellen — und besonders heimtückisch, weil sie oft nur unter Last auftreten. ''@stack_limit(N)'' definiert das maximale Stack-Budget einer Funktion inklusive aller ihrer Aufrufe. Mit ''--stack-check'' analysiert der Compiler den gesamten Call-Graph statisch. @stack_limit(512) fn FilterSensorData(raw: int64, n: int64): f64 { var sum: f64 := 0.0; var buf: f64[16]; // 128 Byte — passt ins Budget var i: int64 := 0; while (i < 16 && i < n) limit(16) { buf[i] := (raw + i * 8) as f64; sum := sum + buf[i]; i := i + 1; } return sum / 16.0; } @stack_limit(1024) fn RunControlCycle(sensors: int64, n: int64): void { var filtered: f64 := FilterSensorData(sensors, n); // nested call var result: ControlOutput := ComputeOutput(filtered); ApplyControl(result); } lyxc flight_ctrl.lyx --stack-check -o flight_ctrl Beispielausgabe bei Grenzwert-Verletzung: [stack-check] RunControlCycle direct locals: 320 bytes FilterSensorData: 192 bytes (inkl. dessen Locals) ComputeOutput: 256 bytes ApplyControl: 88 bytes ───────────────────────────── worst-case total: 856 bytes / 1024 limit — OK [stack-check] FilterSensorData direct locals: 192 bytes / 512 limit — OK [stack-check] PASS — alle Limits eingehalten Wenn ein Limit überschritten wird: error: function 'RunControlCycle' exceeds @stack_limit: worst-case stack: 1280 bytes, limit: 1024 bytes call chain: RunControlCycle → ComputeOutput → FFTTransform (576 bytes) ---- ===== 3. Heap-Allokation mit new / dispose ===== Heap-Speicher überlebt den Scope seiner Allokation. Er wird mit ''new'' angefordert und muss mit ''dispose'' explizit freigegeben werden. ==== Einfache Allokation ==== fn main(): int64 { // Einzelnen Wert auf dem Heap var ptr: int64 := new int64; (ptr as int64) := 42; // Wert schreiben PrintInt(ptr as int64); // Wert lesen PrintStr("\n"); dispose ptr; // Freigabe — danach ist ptr ungültig return 0; } ==== Array auf dem Heap ==== fn AllocBuffer(size: int64): int64 { var buf: int64 := new uint8[size]; // Heap-Array — Größe zur Laufzeit bestimmt return buf; } fn main(): int64 { var n: int64 := 4096; var buffer: int64 := AllocBuffer(n); // Buffer verwenden... FillBuffer(buffer, n, 0); dispose buffer; // Pflicht — kein GC räumt auf return 0; } ==== Struct auf dem Heap ==== type Connection = struct { fd: int64; buf: int64; // Zeiger auf Heap-Puffer buf_size: int64; connected: bool; }; fn NewConnection(fd: int64): int64 { var conn: int64 := new Connection; var c: Connection := conn as Connection; c.fd := fd; c.buf := new uint8[8192]; c.buf_size := 8192; c.connected := true; return conn; } fn FreeConnection(conn: int64): void { var c: Connection := conn as Connection; dispose c.buf; // erst den inneren Puffer freigeben dispose conn; // dann die Struct selbst } > **Reihenfolge bei Freigabe:** Immer von innen nach außen freigeben. Erst den Inhalt, dann den Container — sonst Memory Leak. ---- ===== 4. std.alloc — Direkte Heap-Kontrolle ===== ''std.alloc'' gibt direkten Zugriff auf die Heap-Allokations-Primitive — ohne den ''new''/''dispose''-Syntaxzucker. Das ist die Grundlage aller dynamischen Datenstrukturen in der Standardbibliothek. import std.alloc; fn main(): int64 { // malloc: Speicher allokieren (unkinitialisiert) var ptr: int64 := malloc(1024); if (ptr == 0) { PrintStr("Out of Memory\n"); return 1; } // calloc: Speicher allokieren und auf 0 setzen var zeroed: int64 := calloc(64, 8); // 64 int64 = 512 Byte, alle 0 // realloc: Puffer vergrößern var bigger: int64 := realloc_mem(zeroed, 2048); if (bigger == 0) { free_mem(zeroed); // realloc fehlgeschlagen — Original noch gültig return 1; } zeroed := bigger; // Speicher freigeben free_mem(ptr); free_mem(zeroed); return 0; } ==== malloc_safe vs. malloc_orpanic ==== import std.alloc; fn ProcessLargeDataset(n: int64): void { // malloc_safe: gibt 0 zurück bei OOM — Aufrufer entscheidet var buf: int64 := malloc_safe(n * 8); if (buf == 0) { PrintStr("Kein Speicher — Datensatz übersprungen\n"); return; } // ... verarbeiten ... free_mem(buf); } fn InitSystemBuffers(): void { // malloc_orpanic: bricht das Programm ab bei OOM — für Pflicht-Ressourcen var ring_buf: int64 := malloc_orpanic(65536); // Falls OOM: Programm endet mit Fehlercode — kein weiterer Code wird ausgeführt // ring_buf ist hier garantiert gültig } ^ Funktion ^ Verhalten bei OOM ^ Einsatz ^ | ''malloc'' | Gibt 0 zurück (kein Abbruch) | Wenn OOM behandelt werden soll | | ''malloc_safe'' | Gibt 0 zurück | Explizit: macht Null-Prüfung sichtbar | | ''malloc_orpanic'' | Beendet Programm mit Fehlercode | Pflicht-Ressourcen in der Initialisierung | | ''calloc(n, size)'' | 0 bei OOM | Wenn Nullinitialisierung gewünscht | ==== Alignment ==== ''std.alloc'' garantiert **8-Byte-Alignment** für alle Allokationen — ausreichend für int64, f64 und Zeiger. Für SIMD-Operationen (16/32 Byte) muss manuell ausgerichtet werden: import std.alloc; fn AllocAligned(size: int64, alignment: int64): int64 { // Überschuss allokieren und auf Grenze ausrichten var raw: int64 := malloc(size + alignment); if (raw == 0) { return 0; } var aligned: int64 := (raw + alignment - 1) & -(alignment); return aligned; // Achtung: raw muss gespeichert werden, um es später korrekt freizugeben } ---- ===== 5. Fat Pointers — Arrays mit Längeninformation ===== Dynamische Arrays in Lyx sind **Fat Pointers**: Ein 24-Byte-Struct aus Basisadresse, aktueller Länge (''len'') und Kapazität (''cap''). Der Zeiger zeigt auf den Heap-Puffer, aber Länge und Kapazität werden mitgeführt. ┌─────────────────────────────────────────────┐ │ Fat Pointer (24 Byte auf dem Stack) │ │ base: int64 ──→ [Heap: Elemente] │ │ len: int64 [0][1][2][3]...[n-1] │ │ cap: int64 │ └─────────────────────────────────────────────┘ Das macht Array-Zugriffe sicher: Lyx kann zur Laufzeit prüfen, ob ein Index in [0, len) liegt — ohne dass die Länge separat mitgegeben werden muss. fn main(): int64 { // Statisches Array: kein Fat Pointer — liegt vollständig auf dem Stack var stack_arr: int64[8] := [1, 2, 3, 4, 5, 6, 7, 8]; // Dynamisches Array: Fat Pointer — Heap-Puffer, Länge und Kapazität var dyn_arr: int64[] := new int64[](4); // len=0, cap=4, Heap allokiert // Elemente hinzufügen (wächst automatisch bei Überschreitung der Kapazität) dyn_arr.push(10); dyn_arr.push(20); dyn_arr.push(30); PrintInt(dyn_arr.len); // 3 PrintInt(dyn_arr.cap); // 4 dyn_arr.push(40); dyn_arr.push(50); // cap überschritten → realloc auf cap=8 PrintInt(dyn_arr.len); // 5 PrintInt(dyn_arr.cap); // 8 (verdoppelt) // Freigabe: gibt den Heap-Puffer frei, Fat Pointer selbst ist auf dem Stack dispose dyn_arr; return 0; } Bei ''@redundant'' werden die drei Felder des Fat Pointers (base, len, cap) dreifach im RAM abgelegt und bei jedem Zugriff per Mehrheitsentscheid ausgelesen: @flight_crit fn TrackBuffer(): void { @redundant var secure_buf: int64[] := new int64[](16); // base, len und cap liegen 3× im RAM // Ein Bit-Flip beeinflusst das Ergebnis nicht secure_buf.push(42); dispose secure_buf; } ---- ===== 6. Lebensdauer und Dangling Pointers ===== Da Lyx keinen GC hat, muss der Entwickler Lebensdauer-Regeln einhalten. Der häufigste Fehler: Ein Zeiger auf Speicher, der bereits freigegeben wurde (**Dangling Pointer**). fn GetBuffer(): int64 { var local_buf: uint8[256]; return local_buf as int64; // FEHLER: Zeiger auf Stack-Speicher // local_buf wird freigegeben wenn GetBuffer() zurückkehrt // Der zurückgegebene Zeiger ist danach ungültig } fn GetHeapBuffer(): int64 { var heap_buf: int64 := new uint8[256]; return heap_buf; // OK: Heap-Speicher überlebt den Scope // Aufrufer ist verantwortlich für dispose } ==== Ownership-Konvention ==== Lyx hat kein eingebautes Ownership-System. Die Konvention ist: * Wer ''new'' aufruft, **besitzt** den Speicher * Wer Besitz übergibt (z.B. als Rückgabewert), kommentiert das klar * Funktionen, die einen Zeiger empfangen, geben ihn normalerweise **nicht** frei * Ausnahme: Funktionen mit ''Free''/''Close''/''Destroy'' im Namen // Erzeugt Puffer — Aufrufer ist verantwortlich für dispose fn CreatePacket(size: int64): int64 { return new uint8[size]; } // Verarbeitet Puffer — gibt ihn NICHT frei fn SendPacket(pkt: int64, len: int64): bool { return NetworkSend(pkt, len); } // Gibt Puffer frei — Aufrufer darf ihn danach nicht mehr nutzen fn FreePacket(pkt: int64): void { dispose pkt; } fn main(): int64 { var pkt: int64 := CreatePacket(512); FillPacketHeader(pkt); SendPacket(pkt, 512); FreePacket(pkt); // pkt ist jetzt ungültig — kein weiterer Zugriff return 0; } ---- ===== 7. Speicher in Safety-kritischem Code ===== DO-178C und verwandte Standards stellen klare Anforderungen an Speicherverhalten. Lyx erzwingt die wichtigste Regel mit einem Compiler-Fehler: ''@flight_crit''-Funktionen dürfen kein ''new'' enthalten. @flight_crit fn ControlLoop(state: AircraftState): ControlOutput { var buf: f64[32]; // OK: Stack, Größe zur Compile-Zeit bekannt // var dyn: int64 := new uint8[64]; // Compiler-Fehler: // "heap allocation in @flight_crit function" // Alle Berechnungen auf Stack-Variablen var pitch: f64 := ComputePitch(state); var roll: f64 := ComputeRoll(state); return ControlOutput { pitch: pitch, roll: roll }; } ==== Initialisierungsphase vs. Regelzyklus ==== Das zulässige Muster für dynamischen Speicher in Safety-Code: Einmalige Allokation beim Start, danach ausschließlich Stack. // Globale Zeiger auf vorallokierten Speicher var g_sensor_buf: int64 := 0; var g_log_buf: int64 := 0; con SENSOR_BUF_SIZE: int64 := 4096; con LOG_BUF_SIZE: int64 := 65536; // Initialisierungsphase — new ist erlaubt fn SystemInit(): bool { g_sensor_buf := new uint8[SENSOR_BUF_SIZE]; if (g_sensor_buf == 0) { return false; } g_log_buf := new uint8[LOG_BUF_SIZE]; if (g_log_buf == 0) { dispose g_sensor_buf; return false; } return true; } // Regelzyklus — kein new, nur vorallokierte Puffer @flight_crit @stack_limit(1024) fn RunCycle(sensors: SensorArray): void { // g_sensor_buf wurde in SystemInit allokiert — sicherer Zugriff CopySensorData(sensors, g_sensor_buf, SENSOR_BUF_SIZE); ProcessSensorData(g_sensor_buf, SENSOR_BUF_SIZE); LogData(g_log_buf, LOG_BUF_SIZE); } fn SystemShutdown(): void { dispose g_sensor_buf; dispose g_log_buf; } fn main(): int64 { if (!SystemInit()) { PrintStr("Initialisierung fehlgeschlagen\n"); return 1; } var sensors: SensorArray := ReadSensors(); while (true) limit(2147483647) { RunCycle(sensors); sensors := ReadSensors(); } SystemShutdown(); return 0; } ---- ===== 8. Speicher-Diagnose und Analyse ===== ==== Stack-Verbrauch messen ==== # Statische Stack-Analyse über den gesamten Call-Graph lyxc flight_ctrl.lyx --stack-check # Detaillierter Report mit allen Funktionen und Worst-Case-Pfaden lyxc flight_ctrl.lyx --stack-check --verbose ==== Heap-Nutzung zur Laufzeit prüfen ==== Der Debug-Build des Compilers (mit ''-gh'') verfolgt alle Heap-Allokationen und gibt am Programmende eine Zusammenfassung aus: lyxc-debug myprogram.lyx -o myprogram_debug ./myprogram_debug === Heap Summary === Total allocated: 1 048 576 bytes Total freed: 1 048 576 bytes Peak usage: 131 072 bytes Remaining (leaks): 0 bytes Allocation count: 142 Ein verbleibender Restbetrag (Leaks > 0) zeigt nicht freigegebenen Speicher an. ---- ===== 9. Best Practices ===== ^ Situation ^ Empfehlung ^ | Lokale Variablen | Immer Stack — keine Ausnahme | | Arrays fester Größe | Stack: ''var buf: uint8[256]'' | | Arrays variabler Größe | Heap: ''new uint8[n]'' mit ''dispose'' | | Daten über Scope-Grenzen | Heap — mit klarer Ownership-Konvention | | Pflicht-Ressourcen in Initialisierung | ''malloc_orpanic'' — Programm endet bei OOM | | Optionale Ressourcen | ''malloc_safe'' — Aufrufer prüft auf 0 | | Safety-Code (DAL-A/B) | Nur Stack im Regelzyklus, Heap nur in Init | | ''@flight_crit''-Funktionen | Kein ''new'' — Compiler erzwingt das | | Stack-Budget nachweisen | ''@stack_limit(N)'' + ''--stack-check'' | | TMR für kritische Zustände | ''@redundant'' auf Stack-Variablen | | Speicherlecks aufspüren | Debug-Build mit ''-gh'' | → [[lyx_-_programmiersprache:units:alloc|std.alloc — Vollständige Funktionsreferenz]]\\ → [[lyx_-_programmiersprache:aerospace-safety|Aerospace & Safety — Stack-Limit und TMR im Detail]]