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.
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:
- Cache Misses: Arrays von 24-Byte-Strukturen zerstören die Cache-Lokalität, die für rechenintensive Indikatoren essentiell ist.
- Heap Allokationen: Ein
Value::Recorderzeugt für seine Felder einenRc<Vec<Value>>. Ein einfachespush(series, { price: 100.0, volume: 10 })würde aktuell jedes Mal eine Heap-Allokation für den Record auslösen. - 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:
- Holt die Serie via
downcast_refaus demValue::Object. - Greift direkt auf
series.prices[0]zu. - 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
- Prototyping: Implementierung der primitiven Ringpuffer (
RingBuffer) und der basischen Serien-Strukturen für Skalare und Records. - Specializer-Erweiterung: Erkennung von Typmustern im AST und Generierung der Fast-Path-Knoten (
BoundKind-Erweiterungen). - VM-Integration: Anpassung der Evaluierungs-Loop, um die spezialisierten Knoten auszuführen und via
Any::downcast_refsicher und performant in die nativen Strukturen zu greifen.