FlatExpression POC: data-oriented flat expression tree#511
FlatExpression POC: data-oriented flat expression tree#511
Conversation
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/ac75b2c5-61a5-4e7b-b468-5bfff97971ac Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
dadhi
left a comment
There was a problem hiding this comment.
Too many comments. Keep only important stuff explaining why/how abd remove trivial what.
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/cb3f142a-4345-45e1-af61-2f4161da4ed9 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Done in d565578. Removed all comments that just describe what the code does. Kept only the three non-obvious why/how notes: the Lambda params-in-Info design decision, the |
| // Builds body after registering params so they are found in paramMap when encountered in the body. | ||
| public SysExpr ToSystemExpression() => ToSystemExpression(RootIdx, new Dictionary<int, SysParam>()); | ||
|
|
||
| private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary<int, SysParam> paramMap) |
There was a problem hiding this comment.
Avoid allocations as much as possible. Use SmallMap and such from ImTools. Don't be afraid to add or extend the ImTools if somthing is missing or has errors (apply fix). Use pooling if needed.
There was a problem hiding this comment.
Done in 76ffb84. Replaced Dictionary<int, SysParam> with SmallMap16<int, SysParam, IntEq> (stack-allocated for ≤16 params, uses the AddOrGetValueRef pattern). The paramMap is now default-initialized on the stack and passed ref through the recursive calls.
| return indices[0]; | ||
| } | ||
|
|
||
| public IEnumerable<Idx> Siblings(Idx head) |
There was a problem hiding this comment.
Is it only for tests - comment as such? Otherwise optimize.
There was a problem hiding this comment.
Done in 76ffb84. Siblings is used in tests and as a public convenience API, so it stays — but is now commented as allocating an enumerator. The hot-path usages in SiblingList and the Block vars loop were replaced with direct while (!cur.IsNil) iterations, eliminating the enumerator allocation there.
…aths Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/869f4a36-48ea-4e91-b89b-1079e333c0d4 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
| /// <summary>≥ 0: index into <see cref="ExpressionTree.ClosureConstants"/>. -1: value is inline in Info.</summary> | ||
| public int ConstantIndex; | ||
| public Idx NextIdx; | ||
| public Idx ChildIdx; |
There was a problem hiding this comment.
Let find way to compress the memory layout:
- for constant ChildIndex or ExtraIndex may represent the constant index?
- for up to int (byte/bool - uint) constants we may store the value in extraindex inline?
- can reatreptet (Unsafe) Info and Indexes as a value for constant/other, just put put a byte fields and reinterpet they span based on tbe goal?
- later let think about actual node distribution/invariabts when created - can we compress more given the specific creation patrerns?
Let track struct size in the comments for specific expr type and compare with LightExoresdion analog as a smallest base
There was a problem hiding this comment.
Done in b9df8ea. Here's what changed:
Field reorder → 40→32 bytes: placing the two ref fields (Type, Info) first eliminates the 4-byte padding that LayoutKind.Sequential inserts between the original leading NodeType (int) and the first reference. The struct comment documents the layout and compares to LightExpression heap objects (~40 bytes for Constant/Parameter, ~48–56 for Binary/Unary, all with 16-byte GC header).
ConstantIndex removed: repurposed ExtraIdx as the constant discriminator:
ExtraIdx.It == 0(nil): value is inInfo(boxed reference, or null)ExtraIdx.It > 0:ClosureConstants[ExtraIdx.It - 1](1-based, avoids ambiguity with nil)ExtraIdx.It == -1: inline bits — value packed intoChildIdx.It, no heap allocation
Inline int32 constants: bool, byte, sbyte, char, short, ushort, int, uint, float (via BitConverter.SingleToInt32Bits) all store directly in ChildIdx.It. Larger/reference types (string, long, double, decimal, DateTime, Guid) remain in Info or closure as before.
There was a problem hiding this comment.
@copilot Add minimal meaningful eesential focused summary comment for each public elements to fix the build on ci
Error: /home/runner/work/FastExpressionCompiler/FastExpressionCompiler/src/FastExpressionCompiler/FlatExpression.cs(51,16): error CS1591: Missing XML comment for publicly visible type or member 'Idx.It' [/home/runner/work/FastExpressionCompiler/FastExpressionCompiler/src/FastExpressionCompiler/FastExpressionCompiler.csproj::TargetFramework=netstandard2.0]
…ConstantIndex Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/40e281a9-12ac-41f4-aa81-9a61179a0c47 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Let step back and fo a principled review of the Expressio API differences and FastExpressionCompiler conforming API for those differences: let analyze current diff beteeen SE and fec LE, then to FE. How can we bring the gap and polifill into a (mostly) unified API without decreasing performance? Let compare concrete method calls and parameters representation on some example to make evident. Let reavaluate our current approach with polyfills and usings, etc.and evaluate how to do better. |
dadhi
left a comment
There was a problem hiding this comment.
Address comments and build failures.
…ter.SingleToInt32Bits; add XML docs to all public members Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/8d5fabaa-d5e2-458c-b771-309266696c44 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Fixed in 116a593. Two CI blockers resolved:
Full Release build now passes with 0 warnings and 0 errors across all targets.
Here's the structured analysis and some concrete orthogonal directions. SE vs LE vs FE — where the gap lives
The biggest API friction point: SE and LE both infer result types on arithmetic ops, FE requires an explicit Polyfill idea: extension helpers that mirror SE factory names// In FlatExpression — a thin shim so existing code compiles unchanged
public Idx Add(Idx left, Idx right) => Add(left, right, NodeAt(left).Type);
public Idx Subtract(Idx left, Idx right) => Subtract(left, right, NodeAt(left).Type);This narrows the gap for arithmetic at the cost of a Orthogonal idea 1:
|
Both CI build failures were fixed in 116a593:
The latest CI run ( |
Explores the idea from #512: represent an expression tree as a single flat array of fat structs with integer index references instead of object-graph pointers — enabling stack allocation for small trees, trivial serialization, and O(1) structural equality.
Core types (
src/FastExpressionCompiler/FlatExpression.cs)Idx— 1-basedintindex intoNodes;default(It == 0) is the nil sentinelExpressionNode— 32-byte sequential fat struct (refs-first layout eliminates padding):Type,Info,NodeType,NextIdx(next sibling),ChildIdx(first child / inline constant bits),ExtraIdx(second child / constant discriminator)ExpressionTree— holds nodes inSmallList<ExpressionNode, Stack16<…>, NoArrayPool<…>>(first 16 nodes on the call-stack) and closure constants inSmallList<object, Stack4<…>, …>; factory methods forConstant,Parameter,Unary,Binary,New,Call,Lambda,Conditional,BlockToSystemExpression()— converts toSystem.Linq.Expressionsso existing FEC compilation path is reachable; usesSmallMap16<int, SysParam, IntEq>(stack-resident) instead ofDictionaryto map parameter indices during conversionStructurallyEqual()— O(n) structural comparison via a single pass over the flat arrays; no traversal neededKey design insight surfaced
Lambda parameters cannot be chained via
NextIdx— the same parameter node may already have itsNextIdxoccupied as part of aNew/Callargument chain. Lambda stores its parameters asIdx[]inInfoinstead. This is the central intrusive-list tension: one smallIdx[]allocation per lambda avoids silent list corruption at construction time. A future optimisation could replace it with a (start, count) slice into a dedicated side array.Constant node encoding
ExtraIdxis repurposed as a three-way discriminator, eliminating the oldConstantIndexfield:ExtraIdx.It0(nil)Info(boxed reference, ornull)> 0ClosureConstants[ExtraIdx.It - 1](1-based)-1ChildIdx.It— no boxingTypes stored inline in
ChildIdx.Itwithout boxing:bool,byte,sbyte,char,short,ushort,int,uint,float(reinterpreted via a portable[StructLayout(LayoutKind.Explicit)]union — compatible with all targets including netstandard2.0). Larger types (string,long,double,decimal,DateTime,Guid) remain inInfoor closure.Memory layout
ExpressionNodeis 32 bytes on 64-bit by placing the two reference fields (Type,Info) first — eliminating the 4-byte padding thatLayoutKind.Sequentialwould otherwise insert after the leadingNodeTypeint:vs LightExpression heap objects (16-byte GC header + fields): Constant/Parameter ~40 bytes, Binary/Unary ~48–56 bytes.
Wins
Stack16)ExpressionNodeis 32 bytes (down from 40) — refs-first layout, no paddingint,bool,float, etc.) stored inline — no boxingToSystemExpression()usesSmallMap16(stack-resident) for the parameter map — no heap alloc for ≤ 16 parameterswhile (!cur.IsNil)loops — noIEnumeratorallocationGaps / obstacles
System.Linq.Expressionswithout the adapterInfofield boxesMethodBase/string— one allocation per call/new/parameter nodeTests (
test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs)22 tests covering node construction, parameter identity, structural equality,
ToSystemExpressionconversion, round-trip compile+invoke, and the mutable-closure-constant use case. Guarded with#if !LIGHT_EXPRESSIONsinceFlatExpression.csis not included in the LightExpression variant.