====== 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.
→ [[lyx_-_programmiersprache:units|Standard Library]] · [[lyx_-_programmiersprache:sprache:memory-management|Memory Management]] · [[lyx_-_programmiersprache:sprache:rohspeicher|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 gebunden** — ''malloc()'' 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 [[lyx_-_programmiersprache:sprache:memory-management|Memory Management]] helfen, ''std.alloc'' selbst prüft nichts.
----
**Weiterführend:** [[lyx_-_programmiersprache:sprache:memory-management|Memory Management]] · [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher — peek/poke]] · [[lyx_-_programmiersprache:units|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''.