====== 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''.