Compile-time branch elimination for null/default equality and constant switch expressions#505
Compile-time branch elimination for null/default equality and constant switch expressions#505
Conversation
…itional expressions - Extend TryReduceConditional to eliminate branches when test is null/default equality - Add IsNullDefault helper to check if a type's default value is null - Add TryReduceConditional call in the emit phase as fallback for non-primitive comparisons - Add TryReduceConditional call in the collect phase to skip dead branch closure collection - Add tests covering null==Default, Default==null, Default==Default, and nullable cases Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/babb0376-0bd1-4f18-aaef-62de01a6b8f9 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
dadhi
left a comment
There was a problem hiding this comment.
Cool. There is tied up ussue #489. Let see how can address it too reusing and compressing the logic and preserving consistency with as fast and memory efficient and possible. General question - we need to weight time/compute on the elimination check vs actual emit and work of eliminating jit - benchmark?
- Add Interpreter.TryFindSwitchBranch: detects constant/interpretable switch values and returns the matching case body, eliminating dead case branches at compile time - Supports integer/enum/char (constant + arithmetic via TryInterpretInt) and string - Wire into both collect phase (skips dead branch closure collection) and emit phase - Refactor: move ConvertValueObjectToLong from private EmittingVisitor to internal Interpreter - Add PValueToLong helper in Interpreter for PValue → long conversion - Add 5 tests: int, string, enum, no-match→default, interpreted arithmetic switch values Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/a41fbeeb-293f-421d-93ad-a1b8351344ee Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Addressed issue #489 in commit d4423e8 — added On the benchmark question: the elimination check cost is O(n_cases) comparisons, each O(1) — always dominated by the savings of O(n_non-matching_cases × case_body_complexity) in both closure collection and IL emission. The JIT can't help here since FEC's optimization happens at IL-generation time, before any JIT sees the code. For large switches on constant values (common post-inlining), the speedup can be significant. A dedicated benchmark would be a good follow-up if you want numbers. |
dadhi
left a comment
There was a problem hiding this comment.
Let add a feeling benchamrk were we compare speed of the execution of switch compiled by System.Compile vs compiled by Fec - first for expr. which is not target for Fec branch elimination to introduce a baseline and second for the expr.with some branch eliminated. Use existing benchmark dotnet project in repo. Makes sense?
- New Issue489_Switch_BranchElimination.cs with Compile and Invoke nested classes: - Baseline: Switch(Parameter(...), ...) — runtime value, no FEC branch elimination - Eliminated: Switch(Constant(2), ...) — constant value, FEC emits only matching branch - Both registered in Program.cs (commented-out as per existing convention) Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/03a3da65-f598-4c66-90ce-0545d43edc04 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
| // Models the AutoMapper pattern: after inlining a null argument into a null-check lambda | ||
| public void Condition_with_null_constant_equal_to_default_of_class_type_is_eliminated(TestContext t) | ||
| { | ||
| // Condition(Equal(Constant(null), Default(typeof(string))), Constant("trueBranch"), Constant("falseBranch")) |
There was a problem hiding this comment.
Why do we need this comment if it repeats the actual expression below? I would rather see the reasult of PrintCSharp output for the more compact/evident C# reprrsentation.
After inlining invocations (common in AutoMapper), conditional tests like
Constant(null) == Default(typeof(X))appear — statically evaluable totruebut previously not reduced, causing both branches to be emitted and their closure info collected unnecessarily. Similarly, switch expressions with a constant switch value were emitting all case branches instead of only the matching one.Changes
Conditional branch elimination
TryReduceConditional— extended to eliminate branches when both sides ofEqual/NotEqualrepresent null: aConstantExpressionwithValue == nullor aDefaultExpressionof a reference type, interface, orNullable<T>.IsNullDefaulthelper — newTools.IsNullDefault(Type)usingNullable.GetUnderlyingType()(avoids[RequiresUnreferencedCode]unlikeIsNullable()).TryReduceConditionalfallback afterTryInterpretBoolfails; the latter only handles primitive-typed comparisons, missing reference-type null checks.TryReduceConditionalbefore collecting closure info, so dead branches never contribute unnecessary closure slots.Switch branch elimination (#489)
Interpreter.TryFindSwitchBranch— new method that detects when a switch value is a compile-time constant (or an interpretable arithmetic expression viaTryInterpretInt/TryInterpretPrimitiveValue) and returns only the matching case body. Works for integer/char/enum and string switch values with no custom equality method.TryEmitSwitchcallsTryFindSwitchBranchfirst; on success, emits only the matched branch body.ConvertValueObjectToLongmoved fromprivate EmittingVisitortointernal Interpreterfor shared use. AddedPValueToLonghelper inInterpreter.Examples
Conditional covers:
null == Default(X),Default(X) == null,Default(X) == Default(X), andnull == Default(Nullable<T>)— all with bothEqualandNotEqual.Switch covers: constant integer/enum/char/string switch values and integer arithmetic expressions on constants; falls through to default when no case matches.