Refactor UntypedKind::Parameter to Identifier
The `Parameter` kind in `UntypedKind` was only used for identifiers that were being declared or referenced. This commit renames it to `Identifier` and updates all the necessary code to reflect this change. This simplifies the AST and makes it more consistent. Additionally, a new macro `repeat` has been added to `src/ast/system.myc`.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
*.snap
|
*.snap
|
||||||
*.snap.new
|
*.snap.new
|
||||||
target/
|
target/
|
||||||
|
docs/delphi/
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# 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:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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 `Value`s, 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:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// 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.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
;; Output: 42
|
||||||
|
(do
|
||||||
|
(macro t [x] `(fn [~x] ~x))
|
||||||
|
(def id (t a))
|
||||||
|
(id 42))
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
;; Benchmark: 3.2us
|
;; Benchmark: 2.3us
|
||||||
;; Benchmark-Repeat: 613
|
;; Benchmark-Repeat: 870
|
||||||
;; Output: <StreamNode>
|
;; Output: <StreamNode>
|
||||||
|
|
||||||
(do
|
(do
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
;; Benchmark: 900.6us
|
;; Benchmark: 1.0ms
|
||||||
;; Benchmark-Repeat: 4
|
;; Benchmark-Repeat: 3
|
||||||
;; Tests the effect of record inlining and field lookup optimization
|
;; Tests the effect of record inlining and field lookup optimization
|
||||||
;; Output: 10000
|
;; Output: 10000
|
||||||
(do
|
(do
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
;; Benchmark: 1.5us
|
;; Benchmark: 1.1us
|
||||||
;; Benchmark-Repeat: 1348
|
;; Benchmark-Repeat: 1895
|
||||||
;; Test Record SoA specialization in Pipelines
|
;; Test Record SoA specialization in Pipelines
|
||||||
;; Dank der neuen Spezialisierung wird hierfür im Hintergrund
|
;; Dank der neuen Spezialisierung wird hierfür im Hintergrund
|
||||||
;; eine SharedRecordSeries mit SoA-Layout (Float-Puffer für mid und range) erstellt.
|
;; eine SharedRecordSeries mit SoA-Layout (Float-Puffer für mid und range) erstellt.
|
||||||
|
|||||||
@@ -197,11 +197,6 @@ impl Binder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UntypedKind::Parameter(_) => {
|
|
||||||
diag.push_error("Unexpected 'Parameter' node in general binder context. This should be handled via 'bind_pattern'.", Some(node.identity.clone()));
|
|
||||||
self.make_node(node.identity.clone(), BoundKind::Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
UntypedKind::FieldAccessor(k) => {
|
UntypedKind::FieldAccessor(k) => {
|
||||||
self.make_node(node.identity.clone(), BoundKind::FieldAccessor(*k))
|
self.make_node(node.identity.clone(), BoundKind::FieldAccessor(*k))
|
||||||
}
|
}
|
||||||
@@ -230,7 +225,7 @@ impl Binder {
|
|||||||
|
|
||||||
UntypedKind::Def { target, value } => {
|
UntypedKind::Def { target, value } => {
|
||||||
// Special case: Single identifier (to support recursion)
|
// Special case: Single identifier (to support recursion)
|
||||||
if let UntypedKind::Parameter(ref name) = target.kind {
|
if let UntypedKind::Identifier(ref name) = target.kind {
|
||||||
let addr_opt = self.declare_variable(
|
let addr_opt = self.declare_variable(
|
||||||
name,
|
name,
|
||||||
node.identity.clone(), // Identity of the Def node
|
node.identity.clone(), // Identity of the Def node
|
||||||
@@ -543,7 +538,7 @@ impl Binder {
|
|||||||
diag: &mut Diagnostics,
|
diag: &mut Diagnostics,
|
||||||
) -> BoundNode {
|
) -> BoundNode {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
UntypedKind::Parameter(sym) => {
|
UntypedKind::Identifier(sym) => {
|
||||||
if let Some(addr) = self.declare_variable(sym, node.identity.clone(), kind, diag) {
|
if let Some(addr) = self.declare_variable(sym, node.identity.clone(), kind, diag) {
|
||||||
self.make_node(
|
self.make_node(
|
||||||
node.identity.clone(),
|
node.identity.clone(),
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ impl<E: MacroEvaluator> MacroExpander<E> {
|
|||||||
|
|
||||||
fn extract_param_names(&self, node: &Node<UntypedKind>) -> Result<Vec<Rc<str>>, String> {
|
fn extract_param_names(&self, node: &Node<UntypedKind>) -> Result<Vec<Rc<str>>, String> {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
UntypedKind::Parameter(sym) => Ok(vec![sym.name.clone()]),
|
UntypedKind::Identifier(sym) => Ok(vec![sym.name.clone()]),
|
||||||
UntypedKind::Tuple { elements } => {
|
UntypedKind::Tuple { elements } => {
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
for el in elements {
|
for el in elements {
|
||||||
@@ -372,15 +372,6 @@ impl<E: MacroEvaluator> MacroExpander<E> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
UntypedKind::Parameter(mut sym) => {
|
|
||||||
sym.context = Some(state.expansion_id.clone());
|
|
||||||
Ok(Node {
|
|
||||||
identity: node.identity,
|
|
||||||
kind: UntypedKind::Parameter(sym),
|
|
||||||
ty: (),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UntypedKind::Placeholder(inner) => {
|
UntypedKind::Placeholder(inner) => {
|
||||||
// Break out of template for substitution/evaluation
|
// Break out of template for substitution/evaluation
|
||||||
if let UntypedKind::Identifier(ref sym) = inner.kind
|
if let UntypedKind::Identifier(ref sym) = inner.kind
|
||||||
@@ -654,11 +645,11 @@ mod tests {
|
|||||||
} = &exprs[1].kind
|
} = &exprs[1].kind
|
||||||
{
|
{
|
||||||
if let UntypedKind::Def { target, .. } = &result.kind {
|
if let UntypedKind::Def { target, .. } = &result.kind {
|
||||||
if let UntypedKind::Parameter(sym) = &target.kind {
|
if let UntypedKind::Identifier(sym) = &target.kind {
|
||||||
assert_eq!(sym.context, Some(call.identity.clone()));
|
assert_eq!(sym.context, Some(call.identity.clone()));
|
||||||
assert_eq!(sym.name.as_ref(), "y");
|
assert_eq!(sym.name.as_ref(), "y");
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected Parameter target, got {:?}", target.kind);
|
panic!("Expected Identifier target, got {:?}", target.kind);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected Def result, got {:?}", result.kind);
|
panic!("Expected Def result, got {:?}", result.kind);
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ impl Environment {
|
|||||||
fn discover_globals(&self, node: &Node<UntypedKind>) {
|
fn discover_globals(&self, node: &Node<UntypedKind>) {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
UntypedKind::Def { target, .. } => {
|
UntypedKind::Def { target, .. } => {
|
||||||
if let UntypedKind::Parameter(sym) = &target.kind {
|
if let UntypedKind::Identifier(sym) = &target.kind {
|
||||||
let mut names = self.global_names.borrow_mut();
|
let mut names = self.global_names.borrow_mut();
|
||||||
if !names.contains_key(sym) {
|
if !names.contains_key(sym) {
|
||||||
let idx = GlobalIdx(names.len() as u32);
|
let idx = GlobalIdx(names.len() as u32);
|
||||||
@@ -391,12 +391,11 @@ impl Environment {
|
|||||||
}
|
}
|
||||||
UntypedKind::MacroDecl { name, params, body } => {
|
UntypedKind::MacroDecl { name, params, body } => {
|
||||||
let mut registry = self.macro_registry.borrow_mut();
|
let mut registry = self.macro_registry.borrow_mut();
|
||||||
|
|
||||||
fn extract_names(node: &Node<UntypedKind>) -> Vec<Rc<str>> {
|
fn extract_names(node: &Node<UntypedKind>) -> Vec<Rc<str>> {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
UntypedKind::Parameter(sym) => vec![sym.name.clone()],
|
UntypedKind::Identifier(sym) => vec![sym.name.clone()],
|
||||||
UntypedKind::Tuple { elements } => {
|
UntypedKind::Tuple { elements } => { elements.iter().flat_map(extract_names).collect()
|
||||||
elements.iter().flat_map(extract_names).collect()
|
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -63,8 +63,8 @@ impl Clone for Box<dyn CustomNode> {
|
|||||||
pub enum UntypedKind {
|
pub enum UntypedKind {
|
||||||
Nop,
|
Nop,
|
||||||
Constant(Value),
|
Constant(Value),
|
||||||
|
/// A general identifier (used for both references and declarations in the untyped AST).
|
||||||
Identifier(Symbol),
|
Identifier(Symbol),
|
||||||
Parameter(Symbol),
|
|
||||||
/// A first-class field accessor (e.g. .name)
|
/// A first-class field accessor (e.g. .name)
|
||||||
FieldAccessor(crate::ast::types::Keyword),
|
FieldAccessor(crate::ast::types::Keyword),
|
||||||
If {
|
If {
|
||||||
|
|||||||
+1
-1
@@ -358,7 +358,7 @@ impl<'a> Parser<'a> {
|
|||||||
};
|
};
|
||||||
Node {
|
Node {
|
||||||
identity: NodeIdentity::new(token.location),
|
identity: NodeIdentity::new(token.location),
|
||||||
kind: UntypedKind::Parameter(sym),
|
kind: UntypedKind::Identifier(sym),
|
||||||
ty: (),
|
ty: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
)))
|
)))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(macro repeat [var limit body]
|
||||||
|
`((fn [~var __limit]
|
||||||
|
(if (< ~var __limit)
|
||||||
|
(do
|
||||||
|
~body
|
||||||
|
(again (+ ~var 1) __limit)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
0 ~limit)
|
||||||
|
)
|
||||||
|
|
||||||
;; Creates a stateful cache (Series) from a stateless stream.
|
;; Creates a stateful cache (Series) from a stateless stream.
|
||||||
(macro cache [lookback type src]
|
(macro cache [lookback type src]
|
||||||
`(do
|
`(do
|
||||||
|
|||||||
Reference in New Issue
Block a user