Files
RustAst/docs/todo/record_optimizations_plan_soa.md
T
Michael Schimmel 83324a1892 feat: Document AST node structure across compiler stages
Add a new Markdown document detailing the AST node structure as it
evolves through the compiler's different stages. This includes a diagram
illustrating the data flow and relationships between the generic
`Node<K, T>` structure and its specialized forms (`UntypedNode`,
`BoundNode`, `TypedNode`, `AnalyzedNode`). The document explains the
`kind` and `ty` payloads for each stage and their significance.

Also removes a deleted file related to a previous optimization plan and
adds new files for future optimization plans.
2026-02-27 08:58:38 +01:00

4.6 KiB

Optimierung des Speicherlayouts für Series und Records

Motivation

Der Kern einer jeden DSL für die Finanzanalyse ist die Verarbeitung enormer Mengen an Zeitreihendaten (Ticks, Kerzen, abgeleitete Indikatoren). Diese Daten fließen in Form von Series, Streams und Pipes durch das System.

In der aktuellen Rust-Portierung basiert die Laufzeitumgebung (VM) vollständig auf dem Value-Enum:

pub enum Value {
    Int(i64),
    Float(f64),
    Record(Arc<RecordLayout>, Rc<Vec<Value>>),
    // ...
}

Auf einem 64-Bit-System hat dieses Enum durch Alignment und Tagging eine Größe von exakt 24 Bytes. Jedes Value::Int(42) oder Value::Float(3.14) belegt somit das Dreifache an Speicher im Vergleich zu einem nativen 8-Byte Skalar.

Für einzelne lokale Variablen ist das vernachlässigbar. Für Zeitreihen führt dieses Layout jedoch zu massiven Problemen:

  1. Cache Misses: Arrays von 24-Byte-Strukturen zerstören die Cache-Lokalität, die für rechenintensive Indikatoren essentiell ist.
  2. Heap Allokationen: Ein Value::Record erzeugt für seine Felder einen Rc<Vec<Value>>. Ein einfaches push(series, { price: 100.0, volume: 10 }) würde aktuell jedes Mal eine Heap-Allokation für den Record auslösen.
  3. Pointer Chasing: Das ständige Dereferenzieren von Rc/Arc-Pointern in engen Loops verhindert Vektorisierung (SIMD) auf CPU-Ebene.

In der alten Delphi-Codebase wurde dies durch spezielle Code-Pfade für skalare Typen gelöst. In Rust können wir dank des Specializers und des statischen Typsystems einen noch effizienteren Weg gehen.

Konzept: Data-Oriented Design & Struct of Arrays (SoA)

Anstatt Series als einfache Listen von Value-Objekten zu implementieren, werden Series basierend auf ihrem statischen Typ spezialisiert.

Skalare Serien

Eine Serie von Float-Werten (Series<Float>) speichert intern keine Values, sondern nutzt ein natives Array (z. B. einen Ringpuffer): Vec<f64>.

Record Serien (SoA)

Besonders bei Records zeigt sich die Stärke von SoA. Angenommen, wir haben einen Record Tick { price: Float, volume: Int }. Eine RecordSeries speichert kein Array von Structs (AoS), sondern spaltet die Felder in separate Arrays auf:

// Konzeptionelles Layout
struct TickSeries {
    prices: Vec<f64>, // Perfekt sequenziell im Cache
    volumes: Vec<i64>,
}

Wenn ein Skript z. B. den gleitenden Durchschnitt über prices berechnet, liest die CPU einen dichten Block von f64 direkt in den L1-Cache, ohne jemals das volume laden zu müssen.

Integration in die Compiler-Pipeline

Die Optimierung erfordert keine Änderungen an der Syntax der Skriptsprache. Der AST bleibt aus Sicht des Benutzers dynamisch, wird aber vom Compiler monomorphisiert ("Fast Paths").

1. Das Object Trait

Spezialisierte Serien (wie FloatSeries oder TickSeries) implementieren das bereits vorhandene Object-Trait. Dadurch können sie bei Bedarf in ein Value::Object(Rc<dyn Object>) verpackt und dynamisch durch das System gereicht werden.

2. Node Fusion im Specializer

Da der Analyzer den StaticType jeder Variable kennt, weiß der Specializer zur Compile-Zeit exakt, ob eine Variable eine generische Liste oder eine spezialisierte FloatSeries ist.

Beispiel: Element auslesen let p = my_series[0].price Anstatt einen generischen GetElement und GetField Node zu erzeugen, verschmilzt ("fusioniert") der Specializer dies zu einem spezialisierten VM-Node: ExecNode::GetSeriesRecordFieldFloat. Zur Laufzeit führt dieser Node folgendes aus:

  1. Holt die Serie via downcast_ref aus dem Value::Object.
  2. Greift direkt auf series.prices[0] zu.
  3. Boxed das Ergebnis einmalig in ein Value::Float (oder hält es auf dem Stack).

Beispiel: Werte pushen (Zero-Allocation) push(my_series, { price: 100.0, volume: 10 }) Der Specializer erkennt, dass der Record nur für den Push erzeugt wird. Er generiert einen PushToRecordSeries-Node. Dieser Node erzeugt niemals ein Value::Record auf dem Heap. Stattdessen liest er die Argumente als primitive f64/i64 und schreibt sie direkt in die parallelen Arrays der TickSeries.

Nächste Schritte

  1. Prototyping: Implementierung der primitiven Ringpuffer (RingBuffer) und der basischen Serien-Strukturen für Skalare und Records.
  2. Specializer-Erweiterung: Erkennung von Typmustern im AST und Generierung der Fast-Path-Knoten (BoundKind-Erweiterungen).
  3. VM-Integration: Anpassung der Evaluierungs-Loop, um die spezialisierten Knoten auszuführen und via Any::downcast_ref sicher und performant in die nativen Strukturen zu greifen.