3 Punkte von GN⁺ 2023-09-19 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn man Enums/Tagged Unions mit unterschiedlich großen Varianten in großer Zahl speichert, steigen in Vec und HashMap durch die Reservierung nach der größten Variante die Kosten für Padding und Fragmentierung stark an
  • Zig kann über comptime und Typ-Reflexion Feldgrößen, Alignment und Discriminant prüfen und Enum-Container auf Basis des Speicherlayouts generisch umformen
  • Ein einfaches Vec<Enum> verbraucht pro Element immer so viel Platz wie die größte Variante; SoA reduziert Tag-Padding, lässt aber die Varianten-Fragmentierung im Wertebereich bestehen
  • Dense AoVA, das gleich große Varianten bündelt, reduziert beim Beispiel-Enum die 15 Vektoren auf 3 Cluster mit 2, 4 und 8 Byte, erschwert aber typsichere Iteration, wenn mehrere Varianten in derselben Allocation gemischt werden
  • Rust-Proc-Macros haben nur schwer Zugriff auf Informationen zu Typgröße und Alignment und sind auch bei der Berechnung generischer Array-Längen eingeschränkt, wodurch Zigs typbewusstes Staging die kombinierbare Speichereffizienz von Systemcode besser sichtbar macht

Warum Rust-Enum-Arrays Platz verschwenden

  • Enums/Tagged Unions mit unterschiedlich großen Varianten müssen genug Speicher reservieren, um die größte Variante aufnehmen zu können
  • Das Beispiel-Enum Foo hat Varianten mit u8, u16, u32 und u64; durch Tag und Alignment wird der Typ 16 Byte groß
  • Wenn man viele solcher Enums in Vec oder HashMap ablegt, belegt jedes Element Platz nach dem Maßstab der größten Variante, wodurch Padding und Fragmentierung zunehmen
  • Eine Umwandlung in struct of arrays (SoA) mit separater Allocation für den Tag kann etwas Padding reduzieren, beseitigt aber nicht die Fragmentierung im Wertebereich, die durch unterschiedliche Variantengrößen entsteht
  • Auch in Rust kann man Datenstrukturen für ein bestimmtes Enum von Hand bauen, aber eine für beliebige Enums generische Datenstruktur mit maximaler Speichereffizienz ist schwer oder praktisch nahezu unmöglich
    • Ein Proc-Macro lässt sich bei Typen Dritter oder bei Type Aliases nur schwer mit #[derive] versehen und ist wenig kombinierbar
    • Es fehlt Typbewusstsein, und Workarounds auf Basis von generic_const_expr verteilen ausführliche where-Klauseln über den Call-Graph und harmonieren schlecht mit generischen Typparametern

Warum das Problem bei Compiler-ASTs besonders auffällt

  • Eine der wichtigsten Motivationen für effiziente Enum-Arrays ist der Speicherverbrauch von Compiler-ASTs
  • Große ASTs verursachen während der Kompilierung Memory-Latency und Cache-Eviction und werden so zu einem erheblichen Kostenfaktor für die Frontend-Performance
  • In einem Video über den Carbon-Compiler sagt Chandler Carruth, dass ein geparster clang-AST häufig 50-mal mehr Speicher als der ursprüngliche Quellcode verbraucht
  • Ein Beispiel für die Darstellung von Ausdrucksknoten in Rust besteht aus dem Enum Expr
    • Unit
    • Number
    • Binary(Operation, ExprId, ExprId)
    • Ident(Symbol)
    • Eval(ExprId, ExprSlice)
    • BlockExpression(ExprId, StatementSlice)
  • In OCaml übernimmt das Laufzeitsystem mit dem GC die Speicherverwaltung, sodass sich rekursive Datentypen ohne explizite Indirection ausdrücken lassen
  • In Rust verbraucht Vec<Expr> für jedes Element sizeof(Enum) an Platz, einschließlich der größten Variante sowie Tag und Padding

Fragmentierung mit SoA und AoVA reduzieren

  • Wenn ein einfaches Enum mit drei Varianten Mitglieder von 8, 16 und 32 Bit enthält, reserviert ein normales Vec für alle Elemente viel Platz, damit die 32-Bit-Variante und die Alignment-Anforderungen passen
  • Eine verbreitete Verbesserung ist, mithilfe von Tagged Indices die Enum-Variante selbst klein zu halten
    • das tagged_index-Crate des Rust-Compilers
    • Beispiele für Small-String-Optimization
    • eine häufige Optimierung in Hochleistungscode wie Sprachruntimes, GC, Compilern, Game Engines und OS-Kerneln
  • Man kann auch den Container ändern und mit SoA Discriminant und Werte in separaten Allocations speichern
    • der selbstgehostete Zig-Compiler nutzt diesen Ansatz
    • das durch Tags entstehende Padding wird geringer, aber in der Union-Wertesammlung bleibt Varianten-Fragmentierung bestehen
  • Durch Zigs Staged Compilation lassen sich Container generisch bauen, die eine SoA-Transformation für beliebige Typen durchführen
  • Rust ist dafür auf Proc-Macros wie soa_derive angewiesen und hat die Einschränkung, dass man ohne Änderungen am Quellcode eines Dritttyps kein #[derive] hinzufügen kann

Arrays pro Variante und Clustering nach Größe

  • Um die Fragmentierung im Wertebereich weiter zu verringern, kann man pro Variante einen Vektor verwenden
  • Beim Einfügen gibt man dann einen Tagged Index zurück, der sowohl den Enum-Tag als auch den Index im jeweiligen Varianten-Array enthält
  • Dieses Muster wird array of variant arrays (AoVA) genannt
  • AoVA lässt sich in Rust per Proc-Macro und in Zig mit comptime umsetzen
  • Wenn es viele Varianten gibt und mehrere davon gleich groß sind, führt ein Vektor pro Variante schnell zu zu vielen Vektoren
    • das Beispiel-Enum Foo hat 15 Varianten
    • der Ansatz mit einem Vektor pro Variante fügt 15 Vektoren hinzu
    • Reallocations und System-Calls können zunehmen, und gegenüber einem naiven Vec kann für die Amortisierung mehr Speicher nötig sein
    • die Vektoren können zufällig über den Speicher verteilt sein, was die Wahrscheinlichkeit für Cache-Konflikte erhöht
    • auch der AoVA-Container selbst kann viel Speicher verbrauchen und eingebettete Strukturen aufblähen
  • Gruppiert man stattdessen nach Größe, zerfällt das Beispiel-Enum in drei Cluster: 2 Byte, 4 Byte und 8 Byte
    • c_2: Vec<[u8; 2]> speichert A bis D
    • c_4: Vec<[u8; 4]> speichert E bis I
    • c_8: Vec<[u8; 8]> speichert J bis O
  • Der Dense-AoVA-Ansatz kann die Gesamtzahl der Vektoren um 80 % reduzieren
  • Wenn unterschiedliche Varianten in derselben Allocation zusammenliegen, wird typsichere Iteration über den Vektor schwierig
    • Zugriff ist dann nur über den beim Einfügen erzeugten Tagged Pointer möglich
    • in baumartigen Strukturen auf Basis abgeflachter Indizes, die keine blinde Iteration brauchen, kann das ein akzeptabler Trade-off sein
  • Wenn typsichere Iteration nötig ist, kann man den Tag wieder hinzufügen und die Padding-Kosten in Kauf nehmen
  • Wenn das Padding zu groß wird, lässt sich auf jedes Varianten-Array wieder eine SoA-Transformation anwenden; dann verdoppelt sich allerdings die Zahl der Vektoren

Die kombinierbare Speicherlayout-Effizienz durch Zig-comptime

  • Der Zig-Prototyp ist in osmium implementiert
  • Entscheidend ist die Compile-Time-Reflexion, die über Compiler-Built-ins Feldtypen, Bytegröße, Bitgröße und Discriminant untersucht
  • Im Beispielcode prüft @typeInfo(inner) die Art des Typs und verarbeitet nur den Union-Fall
    • es iteriert über die Union-Felder
    • mit @max(field.alignment, @sizeOf(field.type)) wird der benötigte Platz berechnet
    • Größeninformationen werden in einem stack-allokierten Vektor gespeichert
    • es wird ein Mapping von Union-Feld zu Cluster-Index aufgebaut
    • ist der Typ keine Union, wird ein Compile Error ausgelöst
  • Der genaue Codeschnipsel steht in dieser Quelle
  • Dasselbe Beispiel lässt sich mit einem Rust-Proc-Macro im Grunde nicht erzeugen
    • Proc-Macros haben keinen Zugriff auf Informationen zu Typgröße oder Alignment
    • man könnte zwar ein const fn für die Cluster-Berechnung eines bestimmten Enums erzeugen, es aber nicht zur Festlegung generischer Array-Längen eines generischen Typs verwenden
  • In Rust ist es schwierig, einen generischen Container je nach gegebenem Typ bedingt unterschiedlich zu implementieren, etwa ob es sich um ein Enum oder eine Struct handelt
  • In Zig wäre konzeptionell so etwas möglich wie die Auswahl zwischen EfficientEnumArray<T> und EfficientStructArray<T> abhängig von T.isEnum()
  • Auch die AoVA-Implementierung kann anhand von Enum-Eigenschaften ausgewählt werden
    • zum Beispiel ließe sich spezialisieren, dass das gemeinsame Platzieren unterschiedlicher Varianten nur dann sinnvoll ist, wenn sich dadurch die Zahl der Vektoren um mehr als 90 % reduzieren lässt
  • Wenn die maximale Capacity zur Compile-Zeit bekannt ist, kann die Funktion zur Typerzeugung die für den Tagged Index nötige Bitbreite bestimmen
  • Wenn dieser Tagged Index in anderen Datenstrukturen steckt, etwa in einem weiteren Enum, können verbleibende Bits für den Discriminant genutzt werden
  • Zig erlaubt es, die benötigte Bitzahl konkret festzulegen, sodass andere Teile des Codes diese Information natürlich weiterverwenden können; das ergibt eine kombinierbare Speichereffizienz
  • Dank impliziter Integer-Coercion mit Verbreiterung bleibt die Nutzbarkeit auch bei APIs mit unterschiedlicher Bitbreite erhalten
  • Wer in einer Systemprogrammiersprache Wert auf Effizienz und Zero-Cost-Abstraction legt, sollte sich Staged Programming, insbesondere Zigs comptime, noch einmal ansehen

1 Kommentare

 
GN⁺ 2023-09-19
Meinungen auf Hacker News
  • Es gibt eine andere Strategie, die speichereffizient ist und zugleich die Iteration über die Elemente erhält. Der erste Vektor ist die Liste der Tags, der zweite Vektor enthält die Byte-Offsets der einzelnen Elemente, und der dritte ist weniger ein Vektor als vielmehr die komprimierten Variant-Daten, auf die der zweite Vektor zeigt.
    Damit hat man nur halb so viele Vektoren wie in der endgültigen Lösung des Autors (6 vs. 3), verschwendet keine Padding-Bytes außer dort, wo sie wegen Alignment nötig sind, und die Daten liegen unabhängig vom Typ der Reihe nach im Speicher, sodass man cache-freundlich darüber iterieren kann. Auch ein O(1)-Zugriff auf Elemente per Index ist möglich. Insgesamt hat das für heterogene Daten ähnliche Performance-Eigenschaften wie ein Vec.

    • Byte-Offsets inline zu speichern ist eine gute Idee. Wenn Offsets aber im Speicher abgelegt sind, entsteht beim Iterieren eine Datenabhängigkeit, die trotz Cache-Freundlichkeit in der Prozessor-Pipeline zu erheblichen Speicher-Stalls führen kann.
    • Wenn man eine solche Collection verändern muss, wird man am Ende wahrscheinlich selbst einen Speicher-Allokator verwenden, um Löschungen, Änderungen zu größeren Varianten und Fragmentierung zu behandeln.
    • Wenn man die Offset-Größe nicht optimiert, kann sie im Vergleich zu einem kleinen T recht viel Platz beanspruchen. Ein Beispiel wäre die Kombination aus 64-Bit-size_t und uint8_t T; solange man nur bei der Offset-Größe aufpasst, wirkt der Ansatz vernünftig.
  • Ich frage mich, wie diese AoVA-Datenstruktur in der Praxis funktioniert. Verliert man nicht den indexbasierten Zugriff, da Index-Arithmetik aus Sicht eines Arrays möglicherweise keine Bedeutung mehr hat? Auch die Iteration scheint die Einfügereihenfolge nicht zu bewahren.
    In diesem Kontext halte ich TLV (tag-length-value) mit besseren Caching-Eigenschaften für üblicher. Die Länge kann auch durch den Tag impliziert sein, und zumindest bietet es sinnvolle Vorwärtsiteration. Siehe getdents, inotify und Netlink-Messaging.

    • Wenn man die Bildunterschrift von Abbildung 4 liest, sollte man wohl davon ausgehen, dass das AoVA-Pattern nicht gut passt, wenn die vollständige Reihenfolge der eingefügten Elemente erhalten bleiben muss.
      Im Vergleich zum vorherigen SoA-Layout entsteht keine Gesamtordnung, sondern eine partielle Ordnung. Beim Einfügen bekommt man einen getaggten Index zurück, der sowohl den enum-Tag als auch den Index innerhalb des jeweiligen Variant-Arrays enthält. Daher scheint sortierter Zugriff hier außerhalb des Scopes zu liegen. Wenn man in jedem Element einen globalen Index speichert, könnte man sortierte Iteration wiederherstellen; für sortierten Random Access hilft das aber weiterhin nicht, und der Code würde wahrscheinlich ziemlich viele Branches enthalten.
    • Der beim Einfügen zurückgegebene „enum-Tag und Index innerhalb des jeweiligen Variant-Arrays“ ist im Grunde ein Pointer. Wenn man iterieren möchte, speichert man die Pointer einfach in der gewünschten Nutzungsreihenfolge in einem Array. Genau das tun Programme, die Speicher auf dem Heap allozieren.
      Dinge nach Größe zu speichern, wird auch bei Garbage Collectors und allgemeinen Allokatoren verwendet. Effizienz kann man daraus gewinnen, dass man alle möglichen Objektgrößen kennt, oder durch einfachere Freigabemethoden wie bei einer Arena.
    • Dass AoVA keine eigene Gesamtordnung über die Indizes hat, kann in manchen Anwendungsfällen ein Problem sein, ist aber für die hier vorgeschlagenen AST-Knoten nicht zwingend eines.
      In solchen Fällen kann man die Arrays als Bestandteil einer heap-ähnlichen Struktur betrachten, also wie eine Arena. Der Preis ist, dass der Index zweidimensional sein muss, etwa (tag_idx, va_for_tag_idx). Da die Anzahl der Tags aber zur Compile-Zeit bekannt ist, kann man die Speicherung optimieren, indem man tag_idx in die oberen 4–5 Bits packt und va_for_tag_idx den Rest nutzen lässt. Siehe: https://www.cs.cornell.edu/~asampson/blog/flattening.html
    • Array-Schreibvorgänge, die den Typ eines Index ändern, dürften extrem teuer sein.
  • Ich finde es etwas schade, dass Rusts Pattern Matching nicht eher als eine Art Trait im Typsystem ausgedrückt werden kann, dem beliebige Structs folgen können, statt als expliziter, erstklassiger, fest verdrahteter Objekttyp mit eigener Speicherstruktur.
    Ich habe kürzlich, ähnlich wie in diesem Artikel, sowohl einen AST als auch einen Opcode-/Bytecode-Interpreter implementiert, und Rusts enums fühlen sich für beides nicht ganz ideal an. Beim AST wollte ich jedem Statement-Knoten Zeilen-/Spaltenattribute geben; wenn man Zeile/Spalte in jeden Fall des Stmt-enums einfügt, wird der Boilerplate unschön, und wenn man das enum in ein neues Stmt-Struct einwickelt, das das ursprüngliche enum zusammen mit den Zeilen-/Spaltenattributen enthält, bedeutet das viel Refactoring und wirkt nicht elegant. Auch bei Opcodes ist ein per Pattern Matching behandeltes Rust-enum aus Performance-Sicht für einen VM-Opcode-Interpreter kaum die ideale Codierung, aber die Sprache lenkt einen in diese Richtung, und die Destructuring-Pattern-Funktionalität ist sehr attraktiv. Es scheint Raum für Verbesserungen im Typsystem zu geben, damit man die gewünschte Low-Level-Implementierung verwenden und trotzdem Pattern-Matching-Funktionen bekommen kann.

    • Ein konkreteres Beispiel wäre hilfreich. Das Erste, was mir einfällt, ist eine Art strukturelles Typsystem, aber ich bin nicht sicher, ob ich das genau so richtig verstanden habe.
      https://en.wikipedia.org/wiki/Structural_type_system
    • Eine alte Technik für Bytecode-Interpreter ist, beim Übergang zur Implementierung des nächsten Opcodes einen indirekten Sprung zu verwenden. In gcc gab es dafür die Erweiterung computed goto; in Rust bräuchte man wohl Funktionspointer und etwas, das Tail-Call-Optimierung erzwingt.
      Wenn man einen solchen indirekten Sprung an den Anfang jeder Opcode-Implementierung setzt, kann der indirekte Sprungprädiktor, den CPUs wegen OOP besitzen, für die Enden unterschiedlicher Opcodes separate Modelle haben, wodurch die Vorhersagegenauigkeit steigen kann. Die nächste Instruktion selbst ist zwar schwer vorherzusagen, aber zum Beispiel ist es deutlich wahrscheinlicher, dass auf test ein branch folgt. Allerdings dürften andere Techniken, etwa bei einer Stack Machine den obersten Stack-Wert in einem Register zu halten, wichtiger sein, und ich bin mir nicht sicher, ob diese Technik heute noch relevant ist.
    • Mehrere Sprachen haben Funktionen, die dem ähneln, was du möchtest. Siehe Extractor in Scala oder Active View in F#.
  • Dass „der geparste clang-AST regelmäßig 50-mal mehr Speicher frisst als der ursprüngliche Quellcode“ wirkt ziemlich groß, aber der fehlende Kontext ist: Wie gut kann es überhaupt werden? Wenn man die Quellposition jedes Tokens erhalten und genug Informationen kodieren muss, um sie aus dem AST korrekt wiederherzustellen, frage ich mich, ob der ideale Zuwachs gegenüber dem Original eher 1,5× oder 15× beträgt.

    • Wenn zum Beispiel 30 % weniger Speicher möglich sind, wäre das eine ziemlich große Sache. Wenn man den Compiler dadurch künftig schwerer wartbar macht und nur 30 % einspart, ist es aber vielleicht nicht viel wert. Umgekehrt: Wenn man den Compiler dafür etwas grober anfassen muss, aber 80 % Einsparung erreicht, könnte es sich lohnen.
      Es ist schwer zu sagen, wie hoch die ideale Aufblähung von Source→AST in einer Sprache ist, die sowohl nutzerfreundlich als auch compilerentwicklerfreundlich ist, aber 50× funktioniert immerhin. Im Originaltext wird die 50-fache Aufblähung als Motivation genutzt, eine bestimmte Optimierung zu automatisieren. Es wäre interessant, wenn Rusts enum-Vektoren enum-Werte automatisch in Tags und opaque Werte zerlegen könnten, sodass sie als Struct-of-Arrays gespeichert werden können, wie es der Originaltext in Zig macht. Es gäbe wohl auch nicht viele Stellen, an denen man den Einsatz von unsafe verstecken müsste.
    • Als Vergleich ist das simdjson tape nur ungefähr 3× so groß wie das Originaldokument. Einen großen Teil davon könnte man reduzieren, wenn man Zahlen nur in einem tape-Slot ablegt oder Strings ohne Escape-Sequenzen nicht kopiert, sondern auf ihre Position im Originaldokument verweist.
      Bei Dokumenten, die größtenteils aus []-Zeichen oder 0,-Zeichen bestehen, scheint der maximale Overhead bei etwa 8× zu liegen.
    • Quellcode ist erstaunlich dicht. Als ein Datenpunkt dazu, wie gut es werden kann: der Zig-eigene Parser, wenn er den Zig-eigenen Parser parst.
      Source-Bytes: 139 KiB, Tokens: 24646 (120 KiB), AST-Knoten: 10998 (140 KiB). Jedes Token ist mit 5 Byte ziemlich minimal gehalten (1-Byte-Tag + 4-Byte-Dateioffset), und die AST-Knoten sind ebenfalls dicht und nicht einheitlich kodiert; in diesem Fall etwa 13 Byte pro Knoten. Selbst mit einer solchen Minimalcodierung ist der Parse Tree fast doppelt so groß wie die Quelldatei. Trotzdem ist 2× viel besser als 50×. Quelle: zig ast-check -t lib/std/zig/Parse.zig | head -n7
    • Man sollte sich den verlinkten Vortrag einfach ansehen. Ein großartiger Vortrag. Soweit ich mich erinnere, wurden keine exakten Zahlen genannt; vermutlich war es noch zu früh, um sich sicher zu sein. Es könnten Daten fehlen, von denen man noch nicht erkannt hat, dass man sie braucht, wodurch die Zahlen zu klein ausfallen könnten.
  • Dieser Problemraum fühlt sich wie eine Variante eines Packungsproblems an.
    Es wäre schön, wenn man ausgehend von der endgültigen, für Menschen angenehm zu handhabenden Struktur Empfehlungen für Datenstrukturen erzeugen könnte, die Speicherverschwendung reduzieren, Alignment-Regeln einhalten und die räumliche Lokalität erhöhen. https://en.wikipedia.org/wiki/Packing_problems

  • Es wäre schön, wenn sich proc macros dahin entwickeln würden, dass sie Informationen beim Compiler abfragen können. Dafür bräuchte es wohl ein sorgfältiges Design zusätzlicher Compiler-Phasen, aber Dinge wie „implementiert diese Struktur diesen Trait?“ oder „gib mir die Liste aller konkret implementierten Traits“ sind in proc macros oft sehr nützlich.

    • Wenn ich mich richtig erinnere, führt der Compiler Plugins in zwei Phasen aus. Die erste bekommt den AST vor der Typprüfung und kann ihn verändern; dort laufen macros und einige clippy-Lints. Die zweite Phase ist nach der Typprüfung, erhält also Typinformationen, darf aber nichts mehr verändern; dort laufen andere clippy-Lints.
  • Ich habe den Artikel nur teilweise verstanden, aber aus der Perspektive, eine Spreadsheet-Engine in Rust schreiben zu wollen, wirkt das sehr relevant. Zellwerte brauchen so eine Form:
    pub enum Expr { Number(i32), String(String), Reference(Position), Binary(Box, char, Box), Function(String, Vec), }
    Ich werde weiter lesen und lernen; Hinweise auf Referenzmaterial sind willkommen.

    • Das hier hervorgehobene Problem ist, dass die Varianten sehr unterschiedlich groß sind und bei vielen solchen Werten in einem Array die Performance durch wegen Padding verschwendeten Platz schlechter wird.
      Eine gängige Technik aus dem Games-Bereich ist, ein Array of Structs (AoS) in ein Struct of Arrays (SoA) aufzuteilen. Wenn man zum Beispiel struct Humans { healths: Vec, ammo: Vec, … } hat, entspricht der i-te Index jedes Vektors dem i-ten Human im AoS-Layout. Solche parallelen Vektoren sind nur ein Beispiel und nicht optimal effizient, weil für jedes Feld Länge und Kapazität doppelt Buch geführt werden und dadurch Platz verschwendet wird. Dieser Artikel versucht im Grunde, eine ähnliche Idee automatisch auf enums anzuwenden, was in Rust nicht ohne Weiteres geht. Wie groß dieses Problem in der Praxis ist, könnte etwas übertrieben sein. Bei einer Spreadsheet-Engine sollte man es zunächst nur als mögliche Optimierung im Hinterkopf behalten und zuerst entscheiden, ob man sie für Geschwindigkeit oder für Einfachheit und Verständlichkeit baut.
    • Klingt nach einem interessanten Projekt. Wenn es sich an normale Nutzer richtet, solltest du damit rechnen, dass jemand Inhalte in die vier äußersten Ecken des Sheets setzt und prüft, ob die Engine zusammenbricht.
      Wenn du 1 Million × 1 Million Zellen zulässt und für jede nicht gefüllte Zelle null speicherst, geht dir der Speicher aus. Daher könntest du überlegen, Zellinhalte spärlich zu speichern. Eine Möglichkeit ist die Nutzung einer Hash-Map-Implementierung wie hashbrown. Der Punkt dieses Artikels betrifft Low-Level-Details; wenn du von Anfang an mit einer Hash Map startest und die anfänglichen Speicherbeschränkungen umgehst, musst du im Moment nicht allzu tief darüber nachdenken.
    • Ich habe tatsächlich einmal eine Spreadsheet-Engine in Rust gebaut. Sie ist nicht Open Source, aber ein paar Ratschläge kann ich geben. Bevor du von der Methode in diesem Artikel profitierst, wirst du viele andere Performance-Probleme antreffen.
      Das schwierigste Einzelproblem ist die Evaluierungsstrategie.
  • Wie wäre es mit https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers?

  • Ich glaube, im Beispielcode ist ein Bug:
    field_map[idx] = svec.len - 1;
    Wenn svec bereits size an einer Stelle enthält, die nicht der letzte Eintrag ist, wird das falsch sein.