Skip to content

Commit 47a0678

Browse files
committed
perf: better IL for new things, less reflection
1 parent fc01519 commit 47a0678

2 files changed

Lines changed: 125 additions & 78 deletions

File tree

‎src/FastExpressionCompiler/FastExpressionCompiler.cs‎

Lines changed: 74 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,17 +3000,17 @@ private static bool TryEmitSimpleUnaryExpression(UnaryExpression expr, Expressio
30003000
var endLabel = il.DefineLabel();
30013001

30023002
EmitLoadLocalVariableAddress(il, valueVar);
3003-
il.Demit(OpCodes.Call, typeof(bool?).GetNullableHasValueGetterMethod());
3003+
il.Demit(OpCodes.Call, Tools.NullableReflected<bool>.HasValueGetterMethod);
30043004
il.Demit(OpCodes.Brfalse, nullLabel);
30053005

30063006
EmitLoadLocalVariableAddress(il, valueVar);
3007-
il.Demit(OpCodes.Ldfld, typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
3007+
il.Demit(OpCodes.Ldfld, Tools.NullableReflected<bool>.ValueField);
30083008
if (nodeType == ExpressionType.IsFalse)
30093009
{
30103010
il.Demit(OpCodes.Ldc_I4_0);
30113011
il.Demit(OpCodes.Ceq);
30123012
}
3013-
il.Demit(OpCodes.Newobj, typeof(bool?).GetNullableConstructor());
3013+
il.Demit(OpCodes.Newobj, Tools.NullableReflected<bool>.Constructor);
30143014
il.Demit(OpCodes.Br, endLabel);
30153015

30163016
il.DmarkLabel(nullLabel);
@@ -6062,42 +6062,31 @@ private static bool TryEmitNullableBooleanAndOr(Expression left, Expression righ
60626062
return false;
60636063
var rightVar = EmitStoreLocalVariable(il, typeof(bool?));
60646064

6065-
var absorbLabel = il.DefineLabel();
6066-
var nullLabel = il.DefineLabel();
6065+
var hasValueGetter = Tools.NullableReflected<bool>.HasValueGetterMethod;
6066+
var valueField = Tools.NullableReflected<bool>.ValueField;
6067+
6068+
// The same shape as System emits: no `newobj` and no extra locals, the result is one of the operands.
6069+
// And: left.value ? right : (left.has | right.value) ? left : right
6070+
// Or: left.value ? left : (right.value | left.has) ? right : left
6071+
var sameLabel = il.DefineLabel();
60676072
var doneLabel = il.DefineLabel();
60686073

60696074
EmitLoadLocalVariableAddress(il, leftVar);
6070-
EmitMethodCall(il, typeof(bool?).GetNullableHasValueGetterMethod());
6071-
var leftHasValueLabel = il.DefineLabel();
6072-
il.Demit(OpCodes.Brtrue, leftHasValueLabel);
6073-
6074-
EmitLoadLocalVariableAddress(il, rightVar);
6075-
EmitMethodCall(il, typeof(bool?).GetNullableHasValueGetterMethod());
6076-
il.Demit(OpCodes.Brfalse, nullLabel);
6077-
EmitLoadLocalVariableAddress(il, rightVar);
6078-
il.Demit(OpCodes.Ldfld, typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
6079-
il.Demit(isAnd ? OpCodes.Brtrue : OpCodes.Brfalse, nullLabel);
6080-
il.Demit(OpCodes.Br, absorbLabel);
6075+
il.Demit(OpCodes.Ldfld, valueField);
6076+
il.Demit(OpCodes.Brtrue, sameLabel);
60816077

6082-
il.DmarkLabel(leftHasValueLabel);
60836078
EmitLoadLocalVariableAddress(il, leftVar);
6084-
il.Demit(OpCodes.Ldfld, typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
6085-
il.Demit(isAnd ? OpCodes.Brfalse : OpCodes.Brtrue, absorbLabel);
6086-
EmitLoadLocalVariableAddress(il, rightVar);
6087-
EmitMethodCall(il, typeof(bool?).GetNullableHasValueGetterMethod());
6088-
il.Demit(OpCodes.Brfalse, nullLabel);
6079+
EmitMethodCall(il, hasValueGetter);
60896080
EmitLoadLocalVariableAddress(il, rightVar);
6090-
il.Demit(OpCodes.Ldfld, typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
6091-
il.Demit(OpCodes.Newobj, typeof(bool?).GetNullableConstructor());
6092-
il.Demit(OpCodes.Br, doneLabel);
6081+
il.Demit(OpCodes.Ldfld, valueField);
6082+
il.Demit(OpCodes.Or);
6083+
il.Demit(OpCodes.Brfalse, sameLabel);
60936084

6094-
il.DmarkLabel(absorbLabel);
6095-
il.Demit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1);
6096-
il.Demit(OpCodes.Newobj, typeof(bool?).GetNullableConstructor());
6085+
EmitLoadLocalVariable(il, isAnd ? leftVar : rightVar);
60976086
il.Demit(OpCodes.Br, doneLabel);
60986087

6099-
il.DmarkLabel(nullLabel);
6100-
EmitLoadLocalVariable(il, InitValueTypeVariable(il, typeof(bool?)));
6088+
il.DmarkLabel(sameLabel);
6089+
EmitLoadLocalVariable(il, isAnd ? rightVar : leftVar);
61016090

61026091
il.DmarkLabel(doneLabel);
61036092
return il.EmitPopIfIgnoreResult(parent);
@@ -6116,21 +6105,24 @@ private static bool TryEmitNullableBooleanNot(Expression operand, ILGenerator il
61166105
return false;
61176106
var opVar = EmitStoreLocalVariable(il, typeof(bool?));
61186107

6108+
var hasValueGetter = Tools.NullableReflected<bool>.HasValueGetterMethod;
6109+
var valueField = Tools.NullableReflected<bool>.ValueField;
61196110
var nullLabel = il.DefineLabel();
61206111
var doneLabel = il.DefineLabel();
61216112

61226113
EmitLoadLocalVariableAddress(il, opVar);
6123-
EmitMethodCall(il, typeof(bool?).GetNullableHasValueGetterMethod());
6114+
EmitMethodCall(il, hasValueGetter);
61246115
il.Demit(OpCodes.Brfalse, nullLabel);
61256116
EmitLoadLocalVariableAddress(il, opVar);
6126-
il.Demit(OpCodes.Ldfld, typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
6117+
il.Demit(OpCodes.Ldfld, valueField);
61276118
il.Demit(OpCodes.Ldc_I4_0);
61286119
il.Demit(OpCodes.Ceq);
6129-
il.Demit(OpCodes.Newobj, typeof(bool?).GetNullableConstructor());
6120+
il.Demit(OpCodes.Newobj, Tools.NullableReflected<bool>.Constructor);
61306121
il.Demit(OpCodes.Br, doneLabel);
61316122

6123+
// null stays null, so the operand itself is the result
61326124
il.DmarkLabel(nullLabel);
6133-
EmitLoadLocalVariable(il, InitValueTypeVariable(il, typeof(bool?)));
6125+
EmitLoadLocalVariable(il, opVar);
61346126

61356127
il.DmarkLabel(doneLabel);
61366128
return il.EmitPopIfIgnoreResult(parent);
@@ -6149,49 +6141,59 @@ private static bool TryEmitNullableBooleanShortCircuit(Expression left, Expressi
61496141
return false;
61506142
var leftVar = EmitStoreLocalVariable(il, typeof(bool?));
61516143

6152-
var hasValueGetter = typeof(bool?).GetNullableHasValueGetterMethod();
6153-
var valueField = typeof(bool?).GetNullableValueUnsafeAkaGetValueOrDefaultMethod();
6154-
var shortLabel = il.DefineLabel();
6155-
var leftNullLabel = il.DefineLabel();
6156-
var nullResultLabel = il.DefineLabel();
6144+
var hasValueGetter = Tools.NullableReflected<bool>.HasValueGetterMethod;
6145+
var valueField = Tools.NullableReflected<bool>.ValueField;
6146+
var evalRightLabel = il.DefineLabel();
6147+
var leftResultLabel = il.DefineLabel();
61576148
var doneLabel = il.DefineLabel();
6158-
var decidingOp = isAnd ? OpCodes.Brfalse : OpCodes.Brtrue;
61596149

6160-
EmitLoadLocalVariableAddress(il, leftVar);
6161-
EmitMethodCall(il, hasValueGetter);
6162-
il.Demit(OpCodes.Brfalse, leftNullLabel);
6150+
// The same shape as System emits: the result is one of the operands, no `newobj` and no extra locals
61636151
EmitLoadLocalVariableAddress(il, leftVar);
61646152
il.Demit(OpCodes.Ldfld, valueField);
6165-
il.Demit(decidingOp, shortLabel);
6153+
if (isAnd)
6154+
{
6155+
// left == true: evaluate right, left == false: the result is left, left == null: evaluate right
6156+
il.Demit(OpCodes.Brtrue, evalRightLabel);
6157+
EmitLoadLocalVariableAddress(il, leftVar);
6158+
EmitMethodCall(il, hasValueGetter);
6159+
il.Demit(OpCodes.Brtrue, leftResultLabel);
6160+
}
6161+
else
6162+
il.Demit(OpCodes.Brtrue, leftResultLabel);
61666163

6167-
il.DmarkLabel(leftNullLabel);
6164+
il.DmarkLabel(evalRightLabel);
61686165
if (!TryEmit(right, il, ref context, flags))
61696166
return false;
61706167
var rightVar = EmitStoreLocalVariable(il, typeof(bool?));
61716168

6172-
// left is either null or the non-deciding value, so the result is the right one
6173-
EmitLoadLocalVariableAddress(il, leftVar);
6174-
EmitMethodCall(il, hasValueGetter);
6175-
var leftNullBranch = il.DefineLabel();
6176-
il.Demit(OpCodes.Brfalse, leftNullBranch);
6177-
EmitLoadLocalVariable(il, rightVar);
6178-
il.Demit(OpCodes.Br, doneLabel);
6179-
6180-
il.DmarkLabel(leftNullBranch);
6181-
EmitLoadLocalVariableAddress(il, rightVar);
6182-
EmitMethodCall(il, hasValueGetter);
6183-
il.Demit(OpCodes.Brfalse, nullResultLabel);
6184-
EmitLoadLocalVariableAddress(il, rightVar);
6185-
il.Demit(OpCodes.Ldfld, valueField);
6186-
il.Demit(decidingOp, shortLabel);
6187-
6188-
il.DmarkLabel(nullResultLabel);
6189-
EmitLoadLocalVariable(il, InitValueTypeVariable(il, typeof(bool?)));
6169+
if (isAnd)
6170+
{
6171+
// left.value ? right : right.value ? left : right
6172+
var rightResultLabel = il.DefineLabel();
6173+
EmitLoadLocalVariableAddress(il, leftVar);
6174+
il.Demit(OpCodes.Ldfld, valueField);
6175+
il.Demit(OpCodes.Brtrue, rightResultLabel);
6176+
EmitLoadLocalVariableAddress(il, rightVar);
6177+
il.Demit(OpCodes.Ldfld, valueField);
6178+
il.Demit(OpCodes.Brtrue, leftResultLabel);
6179+
il.DmarkLabel(rightResultLabel);
6180+
EmitLoadLocalVariable(il, rightVar);
6181+
}
6182+
else
6183+
{
6184+
// (right.value | left.has) ? right : left
6185+
EmitLoadLocalVariableAddress(il, rightVar);
6186+
il.Demit(OpCodes.Ldfld, valueField);
6187+
EmitLoadLocalVariableAddress(il, leftVar);
6188+
EmitMethodCall(il, hasValueGetter);
6189+
il.Demit(OpCodes.Or);
6190+
il.Demit(OpCodes.Brfalse, leftResultLabel);
6191+
EmitLoadLocalVariable(il, rightVar);
6192+
}
61906193
il.Demit(OpCodes.Br, doneLabel);
61916194

6192-
il.DmarkLabel(shortLabel);
6193-
il.Demit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1);
6194-
il.Demit(OpCodes.Newobj, typeof(bool?).GetNullableConstructor());
6195+
il.DmarkLabel(leftResultLabel);
6196+
EmitLoadLocalVariable(il, leftVar);
61956197

61966198
il.DmarkLabel(doneLabel);
61976199
return il.EmitPopIfIgnoreResult(parent);
@@ -10131,7 +10133,7 @@ internal static StringBuilder CreateExpressionString(this Expression e, StringBu
1013110133
}
1013210134

1013310135
/// <summary>Provides structural equality comparison for expression trees (both LightExpression and System.Linq.Expressions).
10134-
/// Use the static <see cref="EqualsTo"/> method as the primary entry point — it creates a temporary struct on the stack with no heap allocation.
10136+
/// Use the static <see cref="EqualsTo"/> method as the primary entry point - it creates a temporary struct on the stack with no heap allocation.
1013510137
/// Parameters are matched by their position within their enclosing lambda or block, and label targets by identity pairing.
1013610138
/// Up to 8 lambda parameters and label targets are stored on the stack; larger numbers spill to a heap array.</summary>
1013710139
public struct ExpressionEqualityComparer : IEqualityComparer<Expression>, IEqualityComparer
@@ -10141,7 +10143,7 @@ public struct ExpressionEqualityComparer : IEqualityComparer<Expression>, IEqual
1014110143
private SmallList<ParameterExpression, Stack8<ParameterExpression>, NoArrayPool<ParameterExpression>> _xps, _yps;
1014210144
private SmallList<LabelTarget, Stack8<LabelTarget>, NoArrayPool<LabelTarget>> _xls, _yls;
1014310145

10144-
/// <summary>Structurally compares two expressions. Primary entry point — no heap allocation for the comparer.</summary>
10146+
/// <summary>Structurally compares two expressions. Primary entry point - no heap allocation for the comparer.</summary>
1014510147
[MethodImpl((MethodImplOptions)256)]
1014610148
public static bool EqualsTo(Expression x, Expression y) =>
1014710149
new ExpressionEqualityComparer().Eq(x, y);
@@ -10333,19 +10335,19 @@ private int Hash(Expression expr)
1033310335
}
1033410336
}
1033510337

10336-
/// <summary>IEqualityComparer&lt;Expression&gt; implementation — delegates to the static <see cref="EqualsTo"/> for a fresh context per call.</summary>
10338+
/// <summary>IEqualityComparer&lt;Expression&gt; implementation - delegates to the static <see cref="EqualsTo"/> for a fresh context per call.</summary>
1033710339
[MethodImpl((MethodImplOptions)256)]
1033810340
bool IEqualityComparer<Expression>.Equals(Expression x, Expression y) => EqualsTo(x, y);
1033910341

10340-
/// <summary>IEqualityComparer&lt;Expression&gt; implementation — delegates to the static <see cref="GetHashCode(Expression)"/>.</summary>
10342+
/// <summary>IEqualityComparer&lt;Expression&gt; implementation - delegates to the static <see cref="GetHashCode(Expression)"/>.</summary>
1034110343
[MethodImpl((MethodImplOptions)256)]
1034210344
int IEqualityComparer<Expression>.GetHashCode(Expression obj) => GetHashCode(obj);
1034310345

10344-
/// <summary>Non-generic IEqualityComparer implementation — delegates to the static methods for use with legacy BCL APIs.</summary>
10346+
/// <summary>Non-generic IEqualityComparer implementation - delegates to the static methods for use with legacy BCL APIs.</summary>
1034510347
[MethodImpl((MethodImplOptions)256)]
1034610348
bool IEqualityComparer.Equals(object x, object y) => EqualsTo(x as Expression, y as Expression);
1034710349

10348-
/// <summary>Non-generic IEqualityComparer implementation — delegates to the static <see cref="GetHashCode(Expression)"/>.</summary>
10350+
/// <summary>Non-generic IEqualityComparer implementation - delegates to the static <see cref="GetHashCode(Expression)"/>.</summary>
1034910351
[MethodImpl((MethodImplOptions)256)]
1035010352
int IEqualityComparer.GetHashCode(object obj) => GetHashCode(obj as Expression);
1035110353

‎test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5+
using System.Reflection.Emit;
56
using System.Runtime.CompilerServices;
67
using FastExpressionCompiler.FlatExpression;
78

@@ -63,6 +64,7 @@ public int Run()
6364
Flat_structural_hash_supports_dictionary_lookup();
6465
Can_compile_nullable_boolean_and();
6566
Can_compile_nullable_boolean_or();
67+
Nullable_boolean_logic_emits_the_same_IL_shape_as_system();
6668
Can_compile_shifts_with_nullable_shift_count();
6769
Can_compile_nullable_boolean_and_also();
6870
Can_compile_nullable_boolean_or_else();
@@ -75,7 +77,7 @@ public int Run()
7577
Can_print_reference_comparison();
7678
Can_round_trip_new_expression_kinds_through_system_expressions();
7779

78-
return 57;
80+
return 58;
7981
}
8082

8183
private static int _sideEffectCalls;
@@ -538,6 +540,49 @@ void AssertResult(bool? leftValue, bool? rightValue, bool? expected)
538540
Asserts.Throws<InvalidOperationException>(() => And(left, right, incompatibleMethod));
539541
}
540542

543+
public void Nullable_boolean_logic_emits_the_same_IL_shape_as_system()
544+
{
545+
var l = Parameter(typeof(bool?), "l");
546+
var r = Parameter(typeof(bool?), "r");
547+
548+
Delegate Fast(Expression body) =>
549+
Lambda<Func<bool?, bool?, bool?>>(body, l, r).CompileFast(true, CompilerFlags.EnableDelegateDebugInfo);
550+
551+
Fast(And(l, r)).AssertOpCodes(
552+
OpCodes.Ldarg_1, OpCodes.Stloc_0, OpCodes.Ldarg_2, OpCodes.Stloc_1,
553+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
554+
OpCodes.Ldloca_S, OpCodes.Call, OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Or, OpCodes.Brfalse,
555+
OpCodes.Ldloc_0, OpCodes.Br, OpCodes.Ldloc_1, OpCodes.Ret);
556+
557+
Fast(Or(l, r)).AssertOpCodes(
558+
OpCodes.Ldarg_1, OpCodes.Stloc_0, OpCodes.Ldarg_2, OpCodes.Stloc_1,
559+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
560+
OpCodes.Ldloca_S, OpCodes.Call, OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Or, OpCodes.Brfalse,
561+
OpCodes.Ldloc_1, OpCodes.Br, OpCodes.Ldloc_0, OpCodes.Ret);
562+
563+
Fast(AndAlso(l, r)).AssertOpCodes(
564+
OpCodes.Ldarg_1, OpCodes.Stloc_0,
565+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
566+
OpCodes.Ldloca_S, OpCodes.Call, OpCodes.Brtrue,
567+
OpCodes.Ldarg_2, OpCodes.Stloc_1,
568+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
569+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
570+
OpCodes.Ldloc_1, OpCodes.Br, OpCodes.Ldloc_0, OpCodes.Ret);
571+
572+
Fast(OrElse(l, r)).AssertOpCodes(
573+
OpCodes.Ldarg_1, OpCodes.Stloc_0,
574+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Brtrue,
575+
OpCodes.Ldarg_2, OpCodes.Stloc_1,
576+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Ldloca_S, OpCodes.Call, OpCodes.Or, OpCodes.Brfalse,
577+
OpCodes.Ldloc_1, OpCodes.Br, OpCodes.Ldloc_0, OpCodes.Ret);
578+
579+
Fast(Not(l)).AssertOpCodes(
580+
OpCodes.Ldarg_1, OpCodes.Stloc_0,
581+
OpCodes.Ldloca_S, OpCodes.Call, OpCodes.Brfalse,
582+
OpCodes.Ldloca_S, OpCodes.Ldfld, OpCodes.Ldc_I4_0, OpCodes.Ceq, OpCodes.Newobj, OpCodes.Br,
583+
OpCodes.Ldloc_0, OpCodes.Ret);
584+
}
585+
541586
public void Can_compile_nullable_boolean_or()
542587
{
543588
var left = Parameter(typeof(bool?), "left");
@@ -1146,8 +1191,8 @@ public void Flat_block_variables_and_refs_yield_same_identity()
11461191

11471192
/// <summary>
11481193
/// An outer lambda parameter captured in a nested lambda body creates
1149-
/// a ref node in the nested lambda scope. All three occurrences —
1150-
/// the outer params list, the inner body, and any outer body usage —
1194+
/// a ref node in the nested lambda scope. All three occurrences -
1195+
/// the outer params list, the inner body, and any outer body usage -
11511196
/// must resolve to the exact same SysParameterExpression.
11521197
/// </summary>
11531198
public void Flat_nested_lambda_captures_outer_parameter_identity()
@@ -1259,9 +1304,9 @@ public void Flat_out_of_order_decl_block_in_lambda_compiles_correctly()
12591304
fe.RootIdx = fe.Lambda<Func<int, int>>(block, p);
12601305

12611306
var func = (Func<int, int>)((System.Linq.Expressions.LambdaExpression)fe.ToExpression()).Compile();
1262-
// p=3 → v1 = 3*2=6, v2 = 6+3=9
1307+
// p=3 -> v1 = 3*2=6, v2 = 6+3=9
12631308
Asserts.AreEqual(9, func(3));
1264-
// p=0 → v1 = 0, v2 = 0
1309+
// p=0 -> v1 = 0, v2 = 0
12651310
Asserts.AreEqual(0, func(0));
12661311
}
12671312

@@ -1274,7 +1319,7 @@ enum UIntEnum : uint { A = 0, B = uint.MaxValue }
12741319

12751320
public void Flat_enum_constant_stored_inline_roundtrip()
12761321
{
1277-
// Verify that enum constants with ≤32-bit underlying types are stored inline
1322+
// Verify that enum constants with <=32-bit underlying types are stored inline
12781323
// (no ClosureConstants entry, no boxing) and round-trip correctly.
12791324
void Check<TEnum>(TEnum enumValue) where TEnum : Enum
12801325
{

0 commit comments

Comments
 (0)