Speicher in der Praxis

Lyx hat keinen Garbage Collector und keine Referenzzählung. Wer anlegt, gibt frei — dafür ist die Laufzeit vorhersagbar. Diese Seite zeigt die Gewohnheiten, mit denen das im Alltag funktioniert; die Mittel selbst stehen in Memory Management.

std.alloc · Fehlersuche · Rohspeicher


Drei Wege, Speicher zu bekommen

Weg Freigabe Wofür
Stapelvar x: int64, [N]T, struct automatisch am Blockende Alles, was klein ist und nicht überlebt
Heap rohalloc(n), allocZeroed(n) free(p, n) Puffer, deren Größe erst zur Laufzeit feststeht
Objektnew TKlasse(…) dispose Alles mit Zustand und Methoden

Dazu die libc-nahen Namen aus std.alloc: malloc/calloc/realloc_mem/free_mem (ohne Größenangabe beim Freigeben), malloc_safe und malloc_orpanic (bricht ab, statt 0 zu liefern).

free braucht dieselbe Größe wie die Anforderung. Das ist der Preis dafür, dass kein Kopf vor dem Block liegt — und der häufigste Fehler beim Umstieg von C.


Wer besitzt was

Die Frage, die jeden Speicherfehler entscheidet: Wem gehört dieser Zeiger? Lyx beantwortet sie nicht für dich, deshalb gehört sie in den Quelltext — als Konvention, nicht als Kommentar.

Drei Muster reichen für fast alles:

  1. Der Anleger gibt frei. Eine Klasse, die im Create einen Puffer holt, gibt ihn im Destroy zurück. dispose ruft Destroy.
  2. Der Aufrufer stellt den Puffer (out_buf). Die Funktion schreibt hinein und gibt eine Länge oder einen Fehlercode zurück — sie legt nichts an und gibt nichts frei. So arbeitet die halbe Standardbibliothek (units/validate/*, std.env, InputBox …).
  3. Geliehen, nicht besessen. Ein Zeiger, den eine Funktion nur liest, wird nicht freigegeben. EnvLookupRaw etwa zeigt in die Umgebung; ein free darauf zerstört fremden Speicher.

Beispiel

Alle drei Muster in einem Programm:

import std.io;
import std.alloc;
import std.string;

pub type TListe = class {
  Daten: int64;        // Puffer, uns gehoerend
  Kapazitaet: int64;
  Anzahl: int64;

  fn Create(kap: int64): void {
    self.Kapazitaet := kap;
    self.Anzahl := 0;
    self.Daten := allocZeroed(kap * 8);      // genullt: kein Muell im Puffer
  }

  // Wer anlegt, gibt frei — Destroy raeumt den eigenen Puffer auf
  fn Destroy(): void {
    if (self.Daten != 0) {
      free(self.Daten, self.Kapazitaet * 8);
      self.Daten := 0;
    }
  }

  fn Anhaengen(w: int64): bool {
    if (self.Anzahl >= self.Kapazitaet) { return false; }
    poke64(self.Daten + self.Anzahl * 8, w);
    self.Anzahl := self.Anzahl + 1;
    return true;
  }

  fn Bei(i: int64): int64 {
    if (i < 0 || i >= self.Anzahl) { return 0; }
    return peek64(self.Daten + i * 8);
  }
}

// Ein Puffer, den der AUFRUFER stellt: die Funktion legt nichts an und
// gibt nichts frei. Genau das Muster der Standardbibliothek.
fn SchreibeNamen(out_buf: int64, max: int64): int64 {
  var s: pchar := "Lyx";
  var n: int64 := StrLen(s);
  if (n >= max) { return 0 - 1; }
  var i: int64 := 0;
  while (i < n) {
    poke8(out_buf + i, StrCharAt(s, i));
    i := i + 1;
  }
  poke8(out_buf + n, 0);
  return n;
}

fn main(): int64 {
  // 1. Heapblock von Hand
  var p: int64 := alloc(64);
  if (p == 0) { PrintLn("kein Speicher"); return 1; }
  poke64(p, 42);
  PrintLn(IntToStr(peek64(p)));
  free(p, 64);                    // Groesse muss zur Anforderung passen

  // 2. Objekt: new gehoert dispose
  var l: TListe := new TListe(4);
  l.Anhaengen(10);
  l.Anhaengen(20);
  PrintLn(IntToStr(l.Bei(0) + l.Bei(1)));
  dispose l;                      // ruft Destroy und gibt das Objekt frei

  // 3. Puffer des Aufrufers
  var buf: int64 := alloc(32);
  var n: int64 := SchreibeNamen(buf, 32);
  Print("Name: "); Print(buf as pchar); Print(" ("); Print(IntToStr(n)); PrintLn(" Zeichen)");
  free(buf, 32);

  return 0;
}

42
30
Name: Lyx (3 Zeichen)

Übersetzt und ausgeführt mit lyxc 1.1.3I.


Lecks finden

Es gibt keinen eingebauten Leckprüfer — aber ein Leck ist messbar. Zwei Programme, die dasselbe 200 000-mal tun, einmal mit und einmal ohne free:

/usr/bin/time -v ./leak   2>&1 | grep "Maximum resident"
/usr/bin/time -v ./noleak 2>&1 | grep "Maximum resident"

Maximum resident set size (kbytes): 199936      # ohne free
Maximum resident set size (kbytes): 256         # mit free

Ein Faktor 780 — nachgemessen mit lyxc 1.1.3I. Das Verfahren taugt für jede Schleife, die im Verdacht steht: gleiche Arbeit oft genug wiederholen und den Speicherverbrauch ansehen. Wächst er linear mit der Zahl der Durchläufe, fehlt eine Freigabe.

Für die Zuordnung zur Stelle hilft danach die Absturzstelle über ''%%--%%map-file'' — oder schlicht: die Schleife halbieren, bis das Wachstum verschwindet.


Gewohnheiten, die Ärger ersparen

  • Nach jedem alloc auf 0 prüfen. malloc_orpanic nimmt die Prüfung ab, wenn ein Fehlschlag ohnehin das Ende bedeutet.
  • allocZeroed statt alloc, wenn der Puffer gelesen wird, bevor alles beschrieben ist. Uninitialisierter Heap ist der Grund für „manchmal geht es„.
  • Größe an einer Stelle halten. Wer alloc(n * 8) schreibt, braucht dasselbe n beim free — am besten als Feld daneben.
  • Zeiger nach dem Freigeben auf 0 setzen. Ein doppeltes free ist schlimmer als ein Leck.
  • Puffer nicht zurückgeben, sondern füllen lassen. Eine Funktion, die eine Zeichenkette zurückgibt, muss sagen, wer sie freigibt — eine Funktion mit out_buf nicht.
  • dispose statt free bei Objekten. Nur dispose ruft Destroy.

Wo Lyx mitprüft

  • Eine Variable vom Klassentyp ohne new oder null wird abgewiesen (#1519, auch über Unit-Grenzen #1570).
  • Unter @flight_crit sind alloc, new und lokale Felder verboten — allerdings nur im eigenen Rumpf (#1701).
  • –static-analysis nennt den Stapelbedarf je Funktion; @stack_limit(N) macht daraus eine Zusage.

Letzte Aktualisierung: 2026-08-19 — Beispiel und Leckmessung mit lyxc 1.1.3I nachgemessen; API gegen std/alloc.lyx erhoben.