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 beschriebenmallocund Verwandte als Hüllen umlibc_malloc. Das stimmte nie ganz und stimmt seit lyxc 1.0.17C gar nicht mehr: Die dreilibc_*-Deklarationen hatten keinelink-Klausel und wurden deshalb nie gebunden —malloc()lieferte still0,calloc()schrieb anschließend ab Adresse 0,free_mem()gab nichts frei (#1179). Sie laufen jetzt überalloc/freeund 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 nachfreefä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 auf0setzen.
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_orpanicbricht 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 vonmallocselbst und ruftpanic().
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
freebraucht dieselbe Größe wiealloc. Eine zu kleine Angabe hängt den Block in die falsche Freiliste — der nächsteallocderselben Klasse bekommt dann zu wenig Speicher. Wer die Größe an der Freigabestelle nicht kennt, nimmtmalloc/free_mem.- Familien nicht mischen.
free_memrechnet acht Byte vor dem Zeiger; auf einenalloc-Block angewandt liest es fremden Speicher als Blockgröße. - Zeiger nach
freeauf 0 setzen. Seit dem Pool ist ein Zugriff danach kein sicherer Absturz mehr, sondern stille Korruption. - 1-GiB-Grenze.
allocverweigert alles darüber und liefert0— ohne Meldung. Rückgabewert prüfen, gerade bei berechneten Größen. callocrechnet die Grenze mit.count > 1 GiB / elem_sizeliefert0, 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_MAXgeben 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_checkund die Prüfungen aus Memory Management helfen,std.allocselbst 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.
