Skip to content

Commit b9df8ea

Browse files
Copilotdadhi
andauthored
Compress ExpressionNode: 40→32 bytes, inline small constants, remove 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>
1 parent 76ffb84 commit b9df8ea

2 files changed

Lines changed: 89 additions & 26 deletions

File tree

src/FastExpressionCompiler/FlatExpression.cs

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ public struct Idx : IEquatable<Idx>
6565
/// <summary>
6666
/// Fat node in <see cref="ExpressionTree.Nodes"/>. Intrusive linked-list tree encoding:
6767
/// <list type="table">
68-
/// <item><term>Constant</term> <description>Info = boxed value; ConstantIndex ≥ 0 → value lives in ClosureConstants instead.</description></item>
68+
/// <item><term>Constant</term>
69+
/// <description>
70+
/// ExtraIdx.It == 0 (nil): value is in Info (boxed, or null for null constant).<br/>
71+
/// ExtraIdx.It &gt; 0: value is ClosureConstants[ExtraIdx.It - 1] (1-based).<br/>
72+
/// ExtraIdx.It == -1: int32-fitting value (bool/byte/int/float/…) stored inline in ChildIdx.It bits — no boxing.
73+
/// </description>
74+
/// </item>
6975
/// <item><term>Parameter</term> <description>Info = name (string or null).</description></item>
7076
/// <item><term>Unary</term> <description>Info = MethodInfo (nullable), ChildIdx = operand.</description></item>
7177
/// <item><term>Binary</term> <description>Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right.</description></item>
@@ -75,17 +81,25 @@ public struct Idx : IEquatable<Idx>
7581
/// <item><term>Block</term> <description>ChildIdx = first expr, ExtraIdx = first variable (both chained via NextIdx).</description></item>
7682
/// <item><term>Conditional</term><description>ChildIdx = test, ExtraIdx = ifTrue; ifFalse = ifTrue.NextIdx.</description></item>
7783
/// </list>
84+
/// <para>
85+
/// Layout: 32 bytes on 64-bit (refs first eliminates 4-byte padding after NodeType).<br/>
86+
/// vs LightExpression heap objects (16-byte header + fields):<br/>
87+
/// Constant/Parameter: ~40 bytes heap | Binary/Unary: ~48–56 bytes heap
88+
/// </para>
7889
/// </summary>
7990
[StructLayout(LayoutKind.Sequential)]
80-
public struct ExpressionNode
91+
public struct ExpressionNode // 32 bytes: Type(8)+Info(8)+NodeType(4)+NextIdx(4)+ChildIdx(4)+ExtraIdx(4)
8192
{
82-
public ExpressionType NodeType;
93+
// Reference fields placed first to avoid 4-byte padding that would appear after NodeType.
8394
public Type Type;
8495
public object Info;
85-
/// <summary>≥ 0: index into <see cref="ExpressionTree.ClosureConstants"/>. -1: value is inline in Info.</summary>
86-
public int ConstantIndex;
96+
public ExpressionType NodeType;
8797
public Idx NextIdx;
98+
/// <summary>First child node, or for Constant with ExtraIdx.It==-1: raw int32 value bits.</summary>
8899
public Idx ChildIdx;
100+
/// <summary>
101+
/// Second child node; for Constant: 0=value in Info, positive=ClosureConstants index (1-based), -1=inline bits in ChildIdx.It.
102+
/// </summary>
89103
public Idx ExtraIdx;
90104
}
91105

@@ -116,40 +130,81 @@ private Idx AddNode(
116130
ExpressionType nodeType,
117131
Type type,
118132
object info = null,
119-
int constantIndex = -1,
120133
Idx childIdx = default,
121134
Idx extraIdx = default)
122135
{
123136
ref var n = ref Nodes.AddDefaultAndGetRef();
124137
n.NodeType = nodeType;
125138
n.Type = type;
126139
n.Info = info;
127-
n.ConstantIndex = constantIndex;
128140
n.ChildIdx = childIdx;
129141
n.ExtraIdx = extraIdx;
130142
n.NextIdx = Idx.Nil;
131143
return Idx.Of(Nodes.Count); // Count already incremented by AddDefaultAndGetRef
132144
}
133145

134-
// Primitives with stable identity — safe to keep inline (ConstantIndex == -1).
135-
private static bool IsInlineable(Type t) =>
136-
t == typeof(int) || t == typeof(long) || t == typeof(double) || t == typeof(float) ||
137-
t == typeof(bool) || t == typeof(string) || t == typeof(char) ||
138-
t == typeof(byte) || t == typeof(short) || t == typeof(decimal) ||
139-
t == typeof(DateTime) || t == typeof(Guid);
146+
// Types whose value fits in 32 bits — stored inline in ChildIdx.It to avoid boxing.
147+
private static bool FitsInInt32(Type t) =>
148+
t == typeof(int) || t == typeof(uint) || t == typeof(bool) || t == typeof(float) ||
149+
t == typeof(byte) || t == typeof(sbyte) || t == typeof(short) || t == typeof(ushort) ||
150+
t == typeof(char);
151+
152+
// Encode an inline value as its int32 bit pattern (only call when FitsInInt32 is true).
153+
private static int ToInt32Bits(object value, Type t)
154+
{
155+
if (t == typeof(int)) return (int)value;
156+
if (t == typeof(uint)) return (int)(uint)value; // reinterpret bits
157+
if (t == typeof(bool)) return (bool)value ? 1 : 0;
158+
if (t == typeof(float)) return BitConverter.SingleToInt32Bits((float)value);
159+
if (t == typeof(byte)) return (byte)value;
160+
if (t == typeof(sbyte)) return (sbyte)value;
161+
if (t == typeof(short)) return (short)value;
162+
if (t == typeof(ushort)) return (ushort)value;
163+
if (t == typeof(char)) return (char)value;
164+
return 0; // unreachable
165+
}
166+
167+
// Decode int32 bit pattern back to a boxed value (only call when FitsInInt32 is true).
168+
internal static object FromInt32Bits(int bits, Type t)
169+
{
170+
if (t == typeof(int)) return bits;
171+
if (t == typeof(uint)) return (uint)bits;
172+
if (t == typeof(bool)) return bits != 0;
173+
if (t == typeof(float)) return BitConverter.Int32BitsToSingle(bits);
174+
if (t == typeof(byte)) return (byte)bits;
175+
if (t == typeof(sbyte)) return (sbyte)bits;
176+
if (t == typeof(short)) return (short)bits;
177+
if (t == typeof(ushort)) return (ushort)bits;
178+
if (t == typeof(char)) return (char)bits;
179+
return null; // unreachable
180+
}
181+
182+
// Types not fitting in int32 but still safe to keep inline in Info (no special closure treatment needed).
183+
private static bool IsInfoInline(Type t) =>
184+
t == typeof(string) || t == typeof(long) || t == typeof(double) ||
185+
t == typeof(decimal)|| t == typeof(DateTime)|| t == typeof(Guid);
140186

141187
public Idx Constant(object value, bool putIntoClosure = false)
142188
{
143189
if (value == null)
144-
return AddNode(ExpressionType.Constant, typeof(object), null);
190+
return AddNode(ExpressionType.Constant, typeof(object));
145191

146192
var type = value.GetType();
147-
if (!putIntoClosure && IsInlineable(type))
148-
return AddNode(ExpressionType.Constant, type, value, constantIndex: -1);
193+
if (!putIntoClosure)
194+
{
195+
if (FitsInInt32(type))
196+
// ExtraIdx.It == -1 is the "inline bits" sentinel; ChildIdx.It holds the value.
197+
return AddNode(ExpressionType.Constant, type,
198+
childIdx: new Idx { It = ToInt32Bits(value, type) },
199+
extraIdx: new Idx { It = -1 });
200+
if (IsInfoInline(type))
201+
return AddNode(ExpressionType.Constant, type, info: value);
202+
}
149203

150204
var ci = ClosureConstants.Count;
151205
ClosureConstants.Add(value);
152-
return AddNode(ExpressionType.Constant, type, null, constantIndex: ci);
206+
// ExtraIdx.It > 0 (1-based) identifies the closure constant slot.
207+
return AddNode(ExpressionType.Constant, type, extraIdx: new Idx { It = ci + 1 });
153208
}
154209

155210
public Idx Constant<T>(T value, bool putIntoClosure = false) =>
@@ -271,9 +326,13 @@ private SysExpr ToSystemExpression(Idx nodeIdx, ref SmallMap16<int, SysParam, In
271326
{
272327
case ExpressionType.Constant:
273328
{
274-
var value = node.ConstantIndex >= 0
275-
? ClosureConstants.GetSurePresentRef(node.ConstantIndex)
276-
: node.Info;
329+
object value;
330+
if (node.ExtraIdx.It > 0)
331+
value = ClosureConstants.GetSurePresentRef(node.ExtraIdx.It - 1);
332+
else if (node.ExtraIdx.It == -1)
333+
value = FromInt32Bits(node.ChildIdx.It, node.Type);
334+
else
335+
value = node.Info;
277336
return SysExpr.Constant(value, node.Type);
278337
}
279338

@@ -411,7 +470,6 @@ public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b)
411470
if (na.NodeType != nb.NodeType) return false;
412471
if (na.Type != nb.Type) return false;
413472
if (!InfoEqual(na.Info, nb.Info)) return false;
414-
if (na.ConstantIndex != nb.ConstantIndex) return false;
415473
if (na.NextIdx.It != nb.NextIdx.It) return false;
416474
if (na.ChildIdx.It != nb.ChildIdx.It) return false;
417475
if (na.ExtraIdx.It != nb.ExtraIdx.It) return false;
@@ -443,10 +501,14 @@ public string Dump()
443501
for (var i = 0; i < NodeCount; i++)
444502
{
445503
ref var n = ref Nodes.GetSurePresentRef(i);
504+
var constStr = n.NodeType == ExpressionType.Constant
505+
? (n.ExtraIdx.It > 0 ? $"closure[{n.ExtraIdx.It - 1}]" :
506+
n.ExtraIdx.It == -1 ? $"inline:{FromInt32Bits(n.ChildIdx.It, n.Type)}" :
507+
$"info:{n.Info}")
508+
: null;
446509
sb.AppendLine(
447510
$" [{i + 1}] {n.NodeType,-22} type={n.Type?.Name,-14} " +
448-
$"info={InfoStr(n.Info),-30} " +
449-
$"ci={n.ConstantIndex,2} " +
511+
$"{(constStr != null ? $"val={constStr,-28}" : $"info={InfoStr(n.Info),-28}")} " +
450512
$"child={n.ChildIdx} extra={n.ExtraIdx} next={n.NextIdx}");
451513
}
452514
if (ClosureConstants.Count > 0)

test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ public void Build_constant_node_inline()
7272
ref var node = ref tree.NodeAt(ci);
7373
Asserts.AreEqual(ExpressionType.Constant, node.NodeType);
7474
Asserts.AreEqual(typeof(int), node.Type);
75-
Asserts.AreEqual(42, (int)node.Info);
76-
Asserts.AreEqual(-1, node.ConstantIndex);
75+
Asserts.AreEqual(null, node.Info);
76+
Asserts.AreEqual(-1, node.ExtraIdx.It); // inline bits sentinel
77+
Asserts.AreEqual(42, node.ChildIdx.It); // inline int32 bits
7778
}
7879

7980
public void Build_constant_node_in_closure()
@@ -82,7 +83,7 @@ public void Build_constant_node_in_closure()
8283
var ci = tree.Constant("hello", putIntoClosure: true);
8384

8485
ref var node = ref tree.NodeAt(ci);
85-
Asserts.AreEqual(0, node.ConstantIndex);
86+
Asserts.AreEqual(1, node.ExtraIdx.It); // 1-based closure index
8687
Asserts.AreEqual(1, tree.ClosureConstants.Count);
8788
Asserts.AreEqual("hello", (string)tree.ClosureConstants.GetSurePresentRef(0));
8889
}

0 commit comments

Comments
 (0)