std.alloc

Explizite Speicherverwaltung für Lyx — ohne libc. Der Speicher kommt direkt vom Kernel (mmap mit MAP_ANONYMOUS), kleine Anforderungen aus einem eigenen Pool. std.alloc ist die Grundlage jeder dynamischen Datenstruktur in der Standardbibliothek: Puffer, Listen, Strings, Parser-Zwischenspeicher.

Standard Library · Memory Management · Rohspeicher

Alle Beispiele dieser Seite sind mit lyxc 1.0.21A übersetzt und ausgeführt; die gezeigten Ausgaben sind echte Programmausgaben.

import std.alloc;

Quelle: std/alloc.lyx · Autor: Andreas Röne · Copyright: 2024–2025 Andreas Röne


Zwei Familien, ein Allokator

Die Unit bietet zwei Schnittstellen auf denselben Unterbau. Der Unterschied liegt darin, wer die Blockgröße kennt:

Familie Freigeben mit Größe beim Freigeben Wann nehmen
alloc / allocZeroed free(ptr, size) muss angegeben werden Standardfall in Lyx; kein Verwaltungs-Overhead
malloc / calloc / realloc_mem free_mem(ptr) wird mitgeführt wenn die Größe an der Freigabestelle nicht bekannt ist, oder beim Portieren von C-Code

Die zweite Familie legt dafür einen 8-Byte-Kopf vor jeden Block und gibt den Zeiger dahinter zurück. Das kostet acht Byte je Anforderung — dafür genügt der Zeiger zum Freigeben. Beide Familien lassen sich im selben Programm benutzen, aber nicht mischen: ein alloc-Zeiger darf nicht an free_mem gehen und umgekehrt.

 
Frühere Fassungen dieser Seite beschrieben malloc und Verwandte als Hüllen um libc_malloc. Das stimmte nie ganz und stimmt seit lyxc 1.0.17C gar nicht mehr: Die drei libc_*-Deklarationen hatten keine link-Klausel und wurden deshalb nie gebundenmalloc() lieferte still 0, calloc() schrieb anschließend ab Adresse 0, free_mem() gab nichts frei (#1179). Sie laufen jetzt über alloc/free und sind unit-privat; von außen sichtbar sind nur die hier dokumentierten Funktionen.

Der Kleinspeicher-Pool

Bis 1.0.16C wurde jede Anforderung ein eigenes mmap und jede Freigabe ein munmap. Ein solches Syscall-Paar kostet rund 25 µs — unabhängig davon, ob 32 Byte oder 32 KB angefordert werden. Bei SLH-DSA, das fast nur aus Hash-Aufrufen mit kleinen Kratzpuffern besteht, führte ein einziger KeyGen über 18 000 Paare aus; von 94 Sekunden Laufzeit lagen 58,8 im Kernel (#1258).

Seither gilt:

Anforderung Weg Freigabe
APOOL_MAX (4096 Byte) Größenklasse (nächste Zweierpotenz ab 8), geschnitten aus einem 1-MiB-Block wandert in die Freiliste ihrer Klasse und wird wiederverwendet
> APOOL_MAX eigenes mmap munmap — die Seiten gehen sofort ans System zurück

Die Null-Garantie bleibt. Frischer Speicher kommt genullt vom Kernel; wiederverwendeter wird beim Herausgeben genullt. allocZeroed ist deshalb nur ein anderer Name für alloc.

 
Ein Zugriff nach free fällt nicht mehr zuverlässig auf. Vorher war die Seite wirklich weg und der Zugriff lief in einen Speicherfehler. Jetzt trifft er womöglich einen wiederverwendeten Block — der Fehler war vorher schon einer, er meldet sich nur nicht mehr sofort. Wer Zeiger nach dem Freigeben aufhebt, sollte sie auf 0 setzen.

import std.alloc;
import std.string;

fn main(): int64 {
    // 1. alloc liefert genullten Speicher, free braucht dieselbe Groesse
    var p: int64 := alloc(64);
    PrintLn(StrConcat("frisch genullt: ", IntToStr(peek64(p))));
    poke64(p, 4711);
    free(p, 64);

    // 2. Kleinbloecke (<= APOOL_MAX) kommen aus dem Pool und werden wiederverwendet
    var q: int64 := alloc(64);
    PrintLn(StrConcat("gleiche Adresse wie zuvor: ", IntToStr(p == q)));
    PrintLn(StrConcat("beim Herausgeben genullt:  ", IntToStr(peek64(q))));
    free(q, 64);

    // 3. Grenzen
    PrintLn(StrConcat("alloc(0) liefert Zeiger:   ", IntToStr(alloc(0) != 0)));
    PrintLn(StrConcat("alloc(1 GiB + 1) = 0:      ", IntToStr(alloc(1073741825) == 0)));
    PrintLn(StrConcat("APOOL_MAX:                 ", IntToStr(APOOL_MAX)));
    PrintLn(StrConcat("APOOL_CHUNK:               ", IntToStr(APOOL_CHUNK)));
    return 0;
}

frisch genullt: 0
gleiche Adresse wie zuvor: 1
beim Herausgeben genullt:  0
alloc(0) liefert Zeiger:   1
alloc(1 GiB + 1) = 0:      1
APOOL_MAX:                 4096
APOOL_CHUNK:               1048576


Funktionen

Kern — alloc / free

Signatur Beschreibung
alloc(size: int64): int64 Belegt size Byte, genullt. Liefert die Adresse oder 0 bei Fehlschlag. size ≤ 0 wird als 1 behandelt, size > 1 GiB liefert 0.
allocZeroed(size: int64): int64 Identisch zu alloc — der Speicher ist ohnehin genullt. Nur zur Verdeutlichung der Absicht.
free(ptr: int64, size: int64): void Gibt den Block frei. size muss der Wert aus alloc sein. ptr == 0 ist erlaubt und tut nichts.

C-nahe Familie — malloc / free_mem

Signatur Beschreibung
malloc(size: int64): int64 Belegt size Byte (auf 8 aufgerundet) mit Größenkopf. 0 bei size ≤ 0 oder Fehlschlag. Nicht genullt garantiert — in der Praxis genullt, weil alloc darunter liegt.
calloc(count: int64, elem_size: int64): int64 Belegt count × elem_size Byte und nullt sie ausdrücklich. Prüft auf Überlauf: 0, wenn das Produkt 1 GiB überschreitet oder ein Faktor ≤ 0 ist.
realloc_mem(ptr: int64, new_size: int64): int64 Legt einen neuen Block an, kopiert den alten Inhalt (bis zur kleineren der beiden Größen), gibt den alten frei. ptr == 0 verhält sich wie malloc. 0 bei new_size ≤ 0.
free_mem(ptr: int64): int64 Gibt einen Block dieser Familie frei. Liefert immer ERR_ALLOC_OK; ptr == 0 ist erlaubt.
malloc_safe(size: int64): int64 Wie malloc, mit ausdrücklicher Prüfung auf 0.
malloc_orpanic(size: int64): int64 Wiederholt malloc so lange, bis es gelingt.
 
malloc_orpanic bricht das Programm nicht ab — trotz des Namens. Die Funktion versucht es in einer Endlosschleife weiter; ist der Speicher wirklich erschöpft, hängt das Programm dort, statt mit einem Fehlercode zu enden. Wer sauber abbrechen will, prüft den Rückgabewert von malloc selbst und ruft panic().

Hilfsfunktionen

Signatur Beschreibung
is_aligned(ptr: int64): bool Prüft 8-Byte-Ausrichtung.
size_of_int64(): int64 Liefert 8.
size_of_ptr(): int64 Liefert 8 — Adressen sind in Lyx gewöhnliche int64.

import std.alloc;
import std.string;

fn main(): int64 {
    // malloc kennt seine Groesse selbst - free_mem braucht sie deshalb nicht
    var p: int64 := malloc(100);
    PrintLn(StrConcat("8-Byte-ausgerichtet: ", IntToStr(is_aligned(p))));
    poke64(p, 12345);
    PrintLn(StrConcat("Wert gelesen:        ", IntToStr(peek64(p))));

    // realloc_mem kopiert den alten Inhalt in den neuen Block
    var q: int64 := realloc_mem(p, 4000);
    PrintLn(StrConcat("nach realloc_mem:    ", IntToStr(peek64(q))));
    PrintLn(StrConcat("free_mem liefert:    ", IntToStr(free_mem(q))));

    // calloc nullt und prueft auf Ueberlauf
    var c: int64 := calloc(16, 8);
    PrintLn(StrConcat("calloc genullt:      ", IntToStr(peek64(c + 64))));
    free_mem(c);
    PrintLn(StrConcat("calloc(2^40, 8):     ", IntToStr(calloc(1099511627776, 8))));
    PrintLn(StrConcat("malloc(0):           ", IntToStr(malloc(0))));
    PrintLn(StrConcat("free_mem(0):         ", IntToStr(free_mem(0))));
    PrintLn(StrConcat("size_of_int64:       ", IntToStr(size_of_int64())));
    return 0;
}

8-Byte-ausgerichtet: 1
Wert gelesen:        12345
nach realloc_mem:    12345
free_mem liefert:    0
calloc genullt:      0
calloc(2^40, 8):     0
malloc(0):           0
free_mem(0):         0
size_of_int64:       8


Konstanten

Name Wert Bedeutung
ALIGNMENT 8 Ausrichtung aller Blöcke
ERR_ALLOC_OK 0 kein Fehler
ERR_ALLOC_OOM 1 kein Speicher mehr
ERR_ALLOC_INVALID 2 ungültiges Argument
ERR_ALLOC_ALIGN 3 Ausrichtungsfehler
APOOL_CHUNK 1 048 576 Blockgröße des Kleinspeicher-Pools (1 MiB)
APOOL_MAX 4096 bis hierher aus dem Pool, darüber eigenes mmap

Von den ERR_ALLOC_*-Werten gibt heute nur ERR_ALLOC_OK eine Funktion zurück (free_mem); die übrigen stehen für eigene Fehlerbehandlung bereit. Ein Fehlschlag beim Belegen zeigt sich am Rückgabewert 0.

mmap-Flags

Für den direkten Umgang mit Seiten exportiert die Unit die Flagwerte, die sie selbst benutzt:

Name Wert Bedeutung
MMAP_PROT_NONE 0 keine Rechte
MMAP_PROT_RW 3 lesen + schreiben
MMAP_PROT_RX 5 lesen + ausführen
MMAP_PROT_RWX 7 lesen + schreiben + ausführen
MMAP_ANON 34 MAP_PRIVATE | MAP_ANONYMOUS — anonyme, genullte Seiten
MMAP_SHARED 1 MAP_SHARED für Datei-mmap

import std.alloc;
import std.string;

fn main(): int64 {
    // Eine Seite mit eigenen Rechten holen - alloc nimmt intern denselben Weg
    var seite: int64 := mmap(0, 4096, MMAP_PROT_RW, MMAP_ANON, 0 - 1, 0);
    if (seite < 0) { PrintLn("mmap fehlgeschlagen"); return 1; }
    poke64(seite, 99);
    PrintLn(StrConcat("Wert in der Seite: ", IntToStr(peek64(seite))));
    munmap(seite, 4096);
    return 0;
}

Wert in der Seite: 99

mmap und munmap sind Builtins des Compilers, keine Funktionen dieser Unit — der Dateideskriptor ist bei anonymen Seiten -1, der Offset 0.


Muster: wachsender Puffer

Lyx hat keinen Garbage Collector. Wer einen Puffer vergrößert, legt einen neuen an, kopiert und gibt den alten frei — genau das macht realloc_mem für seine Familie, und so sieht es mit alloc/free aus:

import std.alloc;
import std.string;

// Waechst ein Puffer, wird der alte Inhalt kopiert und der alte Block freigegeben.
fn Grow(alt: int64, altGroesse: int64, neuGroesse: int64): int64 {
    var neu: int64 := alloc(neuGroesse);
    if (neu == 0) { return 0; }
    var i: int64 := 0;
    while (i < altGroesse) { poke8(neu + i, peek8(alt + i)); i := i + 1; }
    free(alt, altGroesse);
    return neu;
}

fn main(): int64 {
    var kap: int64 := 8;             // Kapazitaet in int64-Elementen
    var buf: int64 := alloc(kap * 8);
    var n:   int64 := 0;

    var i: int64 := 0;
    while (i < 20) {
        if (n == kap) {
            buf := Grow(buf, kap * 8, kap * 16);
            kap := kap * 2;
        }
        poke64(buf + n * 8, i * i);
        n := n + 1;
        i := i + 1;
    }

    PrintLn(StrConcat("Elemente:  ", IntToStr(n)));
    PrintLn(StrConcat("Kapazitaet:", IntToStr(kap)));
    PrintLn(StrConcat("letzter:   ", IntToStr(peek64(buf + (n - 1) * 8))));
    free(buf, kap * 8);
    return 0;
}

Elemente:  20
Kapazitaet:32
letzter:   361

Verdoppeln statt schrittweise wachsen: Die Größenklassen des Pools sind ohnehin Zweierpotenzen, ein Wachstum um 1,5 oder 2 trifft sie also genau.


Fallstricke

  • free braucht dieselbe Größe wie alloc. Eine zu kleine Angabe hängt den Block in die falsche Freiliste — der nächste alloc derselben Klasse bekommt dann zu wenig Speicher. Wer die Größe an der Freigabestelle nicht kennt, nimmt malloc/free_mem.
  • Familien nicht mischen. free_mem rechnet acht Byte vor dem Zeiger; auf einen alloc-Block angewandt liest es fremden Speicher als Blockgröße.
  • Zeiger nach free auf 0 setzen. Seit dem Pool ist ein Zugriff danach kein sicherer Absturz mehr, sondern stille Korruption.
  • 1-GiB-Grenze. alloc verweigert alles darüber und liefert 0 — ohne Meldung. Rückgabewert prüfen, gerade bei berechneten Größen.
  • calloc rechnet die Grenze mit. count > 1 GiB / elem_size liefert 0, statt den Überlauf zu provozieren und zu wenig Speicher zu belegen.
  • Der Pool gibt Kleinspeicher nie ans System zurück. Ein 1-MiB-Block bleibt belegt, sobald er angelegt wurde; freigegebene Kleinblöcke werden nur wiederverwendet. Für ein Programm, das kurz sehr viele kleine Blöcke braucht und danach lange läuft, ist das messbar — große Puffer über APOOL_MAX geben ihre Seiten dagegen sofort zurück.
  • Kein Speicherschutz zwischen Blöcken. Ein Überlauf über die Blockgrenze trifft den Nachbarn im selben 1-MiB-Block. @bounds_check und die Prüfungen aus Memory Management helfen, std.alloc selbst prüft nichts.

Weiterführend: Memory Management · Rohspeicher — peek/poke · Standard Library

Letzte Aktualisierung: 2026-08-13 — Seite gegen std/alloc.lyx neu geschrieben: alloc/allocZeroed/free und der Kleinspeicher-Pool (#1258) waren gar nicht dokumentiert, die libc_*-Behauptung ist seit #1179 falsch (die Hüllen sind unit-privat und laufen über alloc). Vier lauffähige Beispiele mit echter Ausgabe ergänzt, malloc_orpanic richtiggestellt (Endlosschleife statt Programmabbruch), mmap-Flags, Grenzwerte und Fallstricke dokumentiert. Geprüft mit lyxc 1.0.17K.