Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ClosureInfo from the NestedLambdaInfo to minimize memory alloc on heap #371

Merged
merged 16 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 96 additions & 97 deletions src/FastExpressionCompiler.LightExpression/Expression.cs

Large diffs are not rendered by default.

641 changes: 244 additions & 397 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs

Large diffs are not rendered by default.

85 changes: 79 additions & 6 deletions src/FastExpressionCompiler/ImTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,39 @@ namespace FastExpressionCompiler.ImTools;

using static FHashMap;

/// <summary>Wrapper for the array and count</summary>
public struct SmallList<T>
{
/// <summary>Array of items</summary>
public T[] Items;
/// <summary>The count of used items</summary>
public int Count;

/// <summary>Creating this stuff</summary>
public SmallList(T[] items, int count)
{
Items = items;
Count = count;
}

/// <summary>Creates the wrapper out of the items</summary>
public SmallList(T[] items) : this(items, items.Length) { }

/// <summary>Popping candy</summary>
public void Pop() => --Count;
}

/// <summary>SmallList module he-he</summary>
public static class SmallList
{
internal const int ForLoopCopyCount = 4;
internal const int InitialCapacity = 4;

[MethodImpl((MethodImplOptions)256)]
internal static void Expand<TItem>(ref TItem[] items)
{
var newItems = new TItem[items.Length << 1]; // have fun to guess the new length, haha ;-P
// `| 1` is for the case when the length is 0
var newItems = new TItem[(items.Length << 1) | 1]; // have fun to guess the new length, haha ;-P
if (items.Length > ForLoopCopyCount)
Array.Copy(items, newItems, items.Length);
else
Expand All @@ -35,18 +59,17 @@ internal static void Expand<TItem>(ref TItem[] items)
/// <summary>Appends the new default item at the end of the items. Assumes that `index lte items.Length`!
/// `items` should be not null</summary>
[MethodImpl((MethodImplOptions)256)]
public static ref TItem AppendDefaultToNotNullItemsAndGetRef<TItem>(ref TItem[] items, int index, int initialCapacity)
public static ref TItem AppendDefaultToNotNullItemsAndGetRef<TItem>(ref TItem[] items, int index)
{
Debug.Assert(index <= items.Length);
if (index == items.Length)
Expand(ref items);
return ref items[index];
}

/// <summary>Appends the new default item at the end of the items. Assumes that `index lte items.Length`!
/// `items` may be null</summary>
/// <summary>Appends the new default item at the end of the items. Assumes that `index lte items.Length`, `items` may be null</summary>
[MethodImpl((MethodImplOptions)256)]
public static ref TItem AppendDefaultAndGetRef<TItem>(ref TItem[] items, int index, int initialCapacity)
public static ref TItem AppendDefaultAndGetRef<TItem>(ref TItem[] items, int index, int initialCapacity = InitialCapacity)
{
if (items == null)
{
Expand All @@ -61,6 +84,56 @@ public static ref TItem AppendDefaultAndGetRef<TItem>(ref TItem[] items, int ind
return ref items[index];
}

/// <summary>Returns surely present item ref by its index</summary>
[MethodImpl((MethodImplOptions)256)]
public static ref TItem GetSurePresentItemRef<TItem>(this ref SmallList<TItem> source, int index) =>
ref source.Items[index];

// todo: @perf add the not null variant
/// <summary>Appends the new default item to the list and returns ref to it for write or read</summary>
[MethodImpl((MethodImplOptions)256)]
public static ref TItem Append<TItem>(ref this SmallList<TItem> source, int initialCapacity = InitialCapacity) =>
ref AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity);

/// <summary>Appends the new item to the list</summary>
// todo: @perf add the not null variant
[MethodImpl((MethodImplOptions)256)]
public static void Append<TItem>(ref this SmallList<TItem> source, in TItem item, int initialCapacity = InitialCapacity) =>
AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item;

/// <summary>Looks for the item in the list and return its index if found or -1 for the absent item</summary>
[MethodImpl((MethodImplOptions)256)]
public static int TryGetIndex<TItem, TEq>(this ref SmallList<TItem> source, TItem it, TEq eq = default)
where TEq : struct, IEq<TItem>
{
var count = source.Count;
var items = source.Items;
for (var i = 0; i < count; ++i)
{
ref var di = ref items[i]; // todo: @perf Marshall?
if (eq.Equals(it, di))
return i;
}
return -1;
}

/// <summary>Returns the ref of the found item or appends the item to the end of the list, and returns ref to it</summary>
[MethodImpl((MethodImplOptions)256)]
public static int GetIndexOrAppend<TItem, TEq>(this ref SmallList<TItem> source, in TItem item, TEq eq)
where TEq : struct, IEq<TItem>
{
var count = source.Count;
var items = source.Items;
for (var i = 0; i < count; ++i)
{
ref var di = ref items[i]; // todo: @perf Marshall?
if (eq.Equals(item, di))
return i;
}
source.Append() = item;
return -1;
}

/// <summary>Returns surely present item ref by its index</summary>
[MethodImpl((MethodImplOptions)256)]
public static ref TItem GetSurePresentItemRef<TItem>(this ref SmallList4<TItem> source, int index)
Expand Down Expand Up @@ -149,7 +222,7 @@ public static int TryGetIndex<TItem, TEq>(this ref SmallList4<TItem> source, TIt

/// <summary>Returns the ref of the found item or appends the item to the end of the list, and returns ref to it</summary>
[MethodImpl((MethodImplOptions)256)]
public static int GetIndexOrAppend<TItem, TEq>(this ref SmallList4<TItem> source, TItem item, TEq eq)
public static int GetIndexOrAppend<TItem, TEq>(this ref SmallList4<TItem> source, in TItem item, TEq eq)
where TEq : struct, IEq<TItem>
{
switch (source._count)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ .NET SDK 7.0.306
| CompileFast | 21.66 us | 0.422 us | 0.330 us | 21.71 us | 1.00 | 0.00 | 1.1902 | 1.1597 | 7.3 KB | 1.00 |
| CompileSys | 506.46 us | 10.053 us | 28.024 us | 495.90 us | 22.69 | 0.93 | 3.9063 | 2.9297 | 27.41 KB | 3.76 |

##

*/
[MemoryDiagnoser]
Expand Down
19 changes: 19 additions & 0 deletions test/FastExpressionCompiler.Benchmarks/NestedLambdasVsVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ .NET SDK 7.0.307
| LightExpression_with_sub_expressions_CompiledFast | 21.32 us | 0.387 us | 0.717 us | 1.00 | 0.00 | 1.4648 | 1.4038 | 0.0305 | 9 KB | 1.00 |
| Expression_with_sub_expressions_Compiled | 571.67 us | 11.221 us | 12.922 us | 26.75 | 1.11 | 3.9063 | 2.9297 | - | 28.47 KB | 3.16 |

| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|-------------------------------------------------- |----------:|----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:|
| LightExpression_with_sub_expressions_CompiledFast | 18.82 us | 0.373 us | 0.894 us | 18.53 us | 1.00 | 0.00 | 1.4648 | 1.3428 | 9.02 KB | 1.00 |
| Expression_with_sub_expressions_Compiled | 508.95 us | 10.150 us | 24.320 us | 498.55 us | 27.09 | 1.65 | 3.9063 | 2.9297 | 28.47 KB | 3.15 |

### Removed ClosureInfo from the LambdaInfo

| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|-------------------------------------------------- |----------:|----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:|
| LightExpression_with_sub_expressions_CompiledFast | 20.96 us | 0.417 us | 1.098 us | 20.67 us | 1.00 | 0.00 | 1.2817 | 1.2512 | 7.99 KB | 1.00 |
| Expression_with_sub_expressions_Compiled | 584.61 us | 25.681 us | 71.163 us | 554.55 us | 28.18 | 4.04 | 3.9063 | 2.9297 | 28.47 KB | 3.56 |

### Optimize the convert from object to avoid calling GetMethods

| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|-------------------------------------------------- |----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:|
| LightExpression_with_sub_expressions_CompiledFast | 19.31 us | 0.321 us | 0.343 us | 1.00 | 0.00 | 1.1902 | 1.1292 | 7.32 KB | 1.00 |
| Expression_with_sub_expressions_Compiled | 602.67 us | 22.702 us | 65.502 us | 30.55 | 3.29 | 3.9063 | 2.9297 | 28.7 KB | 3.92 |

*/
private Expression<Func<A>> _expr;//, _exprWithVars;
private LightExpression.Expression<Func<A>> _lightExpr;
Expand Down
5 changes: 3 additions & 2 deletions test/FastExpressionCompiler.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ public static void Main()
{
// BenchmarkRunner.Run<AccessByRef_vs_ByIGetRefStructImpl>();

// BenchmarkRunner.Run<ApexSerialization_SerializeDictionary.Compile>();
BenchmarkRunner.Run<ApexSerialization_SerializeDictionary.Compile>();

// BenchmarkRunner.Run<EmitHacks.MethodStaticNoArgsEmit>();

BenchmarkRunner.Run<ExprLinqAnyOfNotNullDecimal.Compile>();
// BenchmarkRunner.Run<ExprLinqAnyOfNotNullDecimal.Compile>();
// BenchmarkRunner.Run<ExprLinqAnyOfNotNullDecimal.Invoke>();

// BenchmarkRunner.Run<RepoDb_ListInit.Compile>();
Expand All @@ -32,6 +32,7 @@ public static void Main()
//a.LightExpression_with_sub_expressions_CompiledFast();
//a.Expression_with_sub_expressions_CompiledFast();
//a.Expression_with_sub_expressions_Compiled();

// BenchmarkRunner.Run<NestedLambdasVsVars>();

// BenchmarkRunner.Run<AutoMapper_UseCase_Simplified_OneProperty.Compile_only>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ public void Test_shared_sub_expressions()
Assert.IsNotNull(f);
Assert.IsNotNull(f());

var d = f.TryGetDebugInfo();
// var d = f.TryGetDebugInfo();

// 1, 1 - compiling B in A
// 2, 2 - compiling C in B in A
// 3, 3 - compiling D in C in B in A
// 4 - trying to compile D in B - but already compiled
// 5 - trying to compile C in A - but already compiled
Assert.AreEqual(5, d.NestedLambdaCount);
Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
// Assert.AreEqual(5, d.NestedLambdaCount);
// Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
}

[Test]
Expand All @@ -78,16 +78,16 @@ public void Test_shared_sub_expressions_with_3_dublicate_D()
Assert.IsNotNull(f);
Assert.IsNotNull(f());

var d = f.TryGetDebugInfo();
// var d = f.TryGetDebugInfo();

// 1, 1 - compiling D in A
// 2, 2 - compiling B in A
// 3, 3 - compiling C in B in A
// 4 - trying to compile D in C in B in A - but already compiled
// 5 - trying to compile D in B - but already compiled
// 6 - trying to compile C in A - but already compiled
Assert.AreEqual(6, d.NestedLambdaCount);
Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
// Assert.AreEqual(6, d.NestedLambdaCount);
// Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
}

[Test]
Expand All @@ -104,10 +104,9 @@ public void Test_shared_sub_expressions_with_non_passed_params_in_closure()
Assert.AreEqual("c1", a.C1.Name.Value);
Assert.AreEqual("b1", a.B1.Name.Value);

var d = f.TryGetDebugInfo();

Assert.AreEqual(6, d.NestedLambdaCount);
Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
// var d = f.TryGetDebugInfo();
// Assert.AreEqual(6, d.NestedLambdaCount);
// Assert.AreEqual(3, d.NestedLambdaCompiledTimesCount);
}

[Test]
Expand All @@ -123,10 +122,9 @@ public void Test_2_shared_lambdas_on_the_same_level()
Assert.IsNotNull(dd);
Assert.AreSame(dd.D1, dd.D2);

var d = f.TryGetDebugInfo();

Assert.AreEqual(2, d.NestedLambdaCount);
Assert.AreEqual(1, d.NestedLambdaCompiledTimesCount);
// var d = f.TryGetDebugInfo();
// Assert.AreEqual(2, d.NestedLambdaCount);
// Assert.AreEqual(1, d.NestedLambdaCompiledTimesCount);
}

[Test]
Expand All @@ -144,6 +142,9 @@ public void Test_shared_sub_expressions_assigned_to_vars()
public object GetOrAdd(int i, Func<object> getValue) =>
_objects[i] ?? (_objects[i] = getValue());

public object GetOrPut(int i, Func<object> getValue) =>
_objects[i] = getValue();

private Expression<Func<A>> CreateExpression()
{
var test = Constant(new Nested_lambdas_assigned_to_vars());
Expand Down Expand Up @@ -215,21 +216,21 @@ private Expression<Func<Name, Name, Name, A2>> CreateExpressionWithNonPassedPara
var d1Name = Parameter(typeof(Name), "d1Name");

var d1 = Convert(
Call(test, test.Type.GetMethod(nameof(GetOrAdd)),
Call(test, test.Type.GetMethod(nameof(GetOrPut)),
Constant(2),
Lambda(
New(typeof(D1).GetConstructors()[0], d1Name))),
typeof(D1));

var c1 = Convert(
Call(test, test.Type.GetMethod(nameof(GetOrAdd)),
Call(test, test.Type.GetMethod(nameof(GetOrPut)),
Constant(1),
Lambda(
New(typeof(C1).GetConstructors()[0], d1, c1Name))),
typeof(C1));

var b1 = Convert(
Call(test, test.Type.GetMethod(nameof(GetOrAdd)),
Call(test, test.Type.GetMethod(nameof(GetOrPut)),
Constant(0),
Lambda(
New(typeof(B1).GetConstructors()[0], c1, b1Name, d1))),
Expand Down
3 changes: 2 additions & 1 deletion test/FastExpressionCompiler.TestsRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static void Main()
RunAllTests();
new LightExpression.IssueTests.Issue352_xxxAssign_does_not_work_with_MemberAccess().Run();

// new Nested_lambdas_assigned_to_vars().Run();
// new LightExpression.IssueTests.Nested_lambdas_assigned_to_vars().Run();
// new HoistedLambdaExprTests().Run();
// new Issue366_FEC334_gives_incorrect_results_in_some_linq_operations().Run();
// new LightExpression.UnitTests.NestedLambdasSharedToExpressionCodeStringTest().Run();
// new LightExpression.IssueTests.Issue274_Failing_Expressions_in_Linq2DB().Run();
Expand Down