Skip to content

Reuse extension reductions in both expression compilers (follow-up to #579) - #584

Closed
jogibear9988 wants to merge 3 commits into
dadhi:masterfrom
jogibear9988:fix/reuse-extension-reductions
Closed

jogibear9988 wants to merge 3 commits into
dadhi:masterfrom
jogibear9988:fix/reuse-extension-reductions

Conversation

@jogibear9988

@jogibear9988 jogibear9988 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by #579 and closed to avoid a duplicate merge. The full Extension-reduction fix and its regressions are now included in #579, rebased onto upstream master 65ef6641 as commit 183ea000. No changes from this follow-up remain outside #579. The original proposal and measurements below are retained as historical evidence; no separate merge is needed.


Follow-up to #579; please merge the parent first. The branch starts at its exact head fcc70222f4b2b387ed7fa6bbd6c4092287f646e9 and adds one fix commit (08b190e9). GitHub's formal base is master because the #579 branch lives in the fork. Review the incremental follow-up diff; #579 itself is unchanged.

Problem and fix

Ordinary Extension nodes are reduced separately during collection and emission. A reducer that creates fresh locals, captured constants, labels or nested lambdas can therefore emit a different tree from the one whose identities were collected. The new regression fails strict compilation on #579 for both Standard and LightExpression, with Reduce called twice. With this follow-up, the same test returns the expected result and Reduce runs once.

Extend #579's existing lazy reduction cache to the shared Extension path. Both visitors use the same helper and the same reduced tree. The cache remains local to each lambda compilation and keyed by reference identity, using FEC's existing SmallMap. No new compiler traversal, expression rewrite phase or closure layout is introduced. Light intrinsic callbacks retain their existing dispatch before this path.

The policy matches #579: one reduced representation per source node per lambda compilation, including repeated references to that node. Distinct node instances remain separate even if they override equality; compiling the same source again obtains new reductions. No global cache is used.

System's unknown custom node kinds retain CanReduce/ReduceAndCheck validation. Extension nodes keep the existing null/self rejection contract shared with LightExpression. The existing reduction-progress test is updated to assert reuse of a valid first result, rather than requiring an unnecessary second reduction during emission. Invalid first reductions remain rejected, including emission without collection.

Validation

  • Seven shared regression methods run for Standard and LightExpression; one additional Light method verifies intrinsic collection/emission hooks run without invoking Reduce.
  • Covers fresh block locals and captured objects, nested lambda identities, fresh/shared labels, shared versus distinct nodes, conditional branch ordering, separate compilations, CompileFastToIL, TryCompileWithoutClosure, and null/self rejection. Tree-identity cases exercise default, no-inlining and strict/debug flags.
  • All new regressions pass on .NET 8, .NET 10 and .NET Framework 4.7.2. Strict FEC compilation is used; System fallback cannot satisfy them.
  • Full Release solution build passes with zero warnings/errors.
  • Legacy native suites pass: 1,756 on .NET 10 and 1,755 on .NET 8.
  • Framework runner passes 213 TestX + 1,639 legacy cases (parent: 198 + 1,639). No tests are skipped or removed.
  • Modern full runners retain only the parent's nullable MultiplyChecked parity failures for (1,1) and (2,1), in both APIs. Parent and follow-up were both rebuilt/rerun; failure content is identical (parallel console line ordering can differ). Legacy suites were invoked separately because that existing TestX failure gates the normal runner.

Performance and allocation cost

Compare only the exact #579 parent above with this follow-up. Windows ARM64, SDK 10.0.301, .NET 8.0.28/10.0.9, Release; tiering disabled, fixed CPU affinity. Expression construction/Light conversion is outside timed compilation. Strict FEC; 300 compilation warmups and 100,000 invocation warmups, two opposite-load-order processes, nine alternating samples per side, approximately 24 ms pair-calibration target, GC before samples and a no-GC region where available. Times below combine process medians geometrically.

Tradeoff: each tested single-extension compilation adds 120 B for the lazy cache, in both APIs. A stable reducer pays this cost too: the compiler cannot assume a second Reduce call would return the same tree. Larger reduction sets can require additional map storage. The cache is compilation-local; compiled delegates do not retain it.

Ordinary add, reference-constant and constructor controls retain the same compilation allocations. All measured controls retain identical decoded emitted IL and the same invocation allocations. Constructor invocation intentionally allocates its Marker object (24 B); other invocation controls allocate zero. The previously broken fresh-reduction cases are correctness tests, not a valid before/after performance baseline.

Timing remains noisy, with slower observations included below. No speedup or zero-compilation-overhead claim is made. Runtime timing differences below are observations of identical emitted control IL. Framework is correctness-tested; its timing was not measured.

Runtime API Control Compile ns, #579 → follow-up Change Invoke ns, #579 → follow-up Compile B/op Invoke B/op, both
net10.0 Standard add_control 1036.6 → 1038.7 +0.2% 1.86 → 1.87 872 → 872 0
net10.0 Standard reference_constant 945.4 → 949.7 +0.5% 1.84 → 1.81 992 → 992 0
net10.0 Standard new_intrinsic_control 941.3 → 951.6 +1.1% 3.54 → 3.76 896 → 896 24
net10.0 Standard stable_extension_leaf 1098.9 → 1153.6 +5.0% 2.19 → 2.15 872 → 992 0
net10.0 Standard stable_extension_block 1353.6 → 1407.3 +4.0% 1.91 → 1.99 1032 → 1152 0
net10.0 Light add_control 953.9 → 1003.6 +5.2% 1.81 → 1.83 872 → 872 0
net10.0 Light reference_constant 979.0 → 890.8 -9.0% 1.86 → 1.81 992 → 992 0
net10.0 Light new_intrinsic_control 739.1 → 698.0 -5.6% 3.47 → 3.55 896 → 896 24
net10.0 Light stable_extension_leaf 1047.7 → 994.3 -5.1% 2.10 → 2.12 872 → 992 0
net10.0 Light stable_extension_block 1405.9 → 1442.6 +2.6% 1.96 → 1.92 1032 → 1152 0
net8.0 Standard add_control 946.4 → 954.8 +0.9% 1.85 → 1.88 872 → 872 0
net8.0 Standard reference_constant 980.9 → 946.7 -3.5% 1.85 → 1.90 992 → 992 0
net8.0 Standard new_intrinsic_control 826.5 → 850.6 +2.9% 3.18 → 3.21 896 → 896 24
net8.0 Standard stable_extension_leaf 1036.0 → 1092.9 +5.5% 2.11 → 2.13 872 → 992 0
net8.0 Standard stable_extension_block 1296.8 → 1389.7 +7.2% 2.10 → 2.11 1032 → 1152 0
net8.0 Light add_control 924.3 → 942.0 +1.9% 1.82 → 1.85 872 → 872 0
net8.0 Light reference_constant 963.6 → 923.8 -4.1% 1.89 → 1.88 992 → 992 0
net8.0 Light new_intrinsic_control 719.6 → 687.3 -4.5% 3.29 → 3.18 896 → 896 24
net8.0 Light stable_extension_leaf 912.4 → 931.5 +2.1% 2.12 → 2.13 872 → 992 0
net8.0 Light stable_extension_block 1283.3 → 1183.6 -7.8% 2.11 → 2.13 1032 → 1152 0

Identical-binary parent/parent compilation control ranges across the two process medians:

Runtime Minimum Maximum
net10.0 -6.3% +17.2%
net8.0 -15.6% +19.1%

Use CanReduce and ReduceAndCheck for unknown System expression node kinds in the existing collector and emitter. Keep validation in a private cold helper; preserve label operand scope. Test reduction chains, captured constants and invalid result types.
Keep reduced trees in a lazy per-compilation reference-identity cache so collection and emission observe the same locals, constants, and nested lambdas. Preserve ReduceAndCheck validation and leave LightExpression unchanged.

Add regressions for fresh reductions, shared and distinct node identities, repeated compilation, and emission without collection. Verified all regression cases on net8.0, net10.0 and net472; full Release build passes. Existing master nullable MultiplyChecked parity failures remain unchanged.
Extend the lazy per-compilation reduction cache from dadhi#579 to ExpressionType.Extension for Standard and LightExpression. Collection and emission use the same reduced locals, constants, labels and nested lambdas. Preserve System validation for custom node kinds and existing null/self reduction rejection; intrinsic callbacks remain unchanged.

Retain the parent per-node reference-identity policy: shared nodes reduce once per lambda compilation, distinct nodes stay separate, and later compilations get fresh reductions. Update the existing progress regression to assert reuse of a valid first reduction instead of requiring a second emission-time reduction.

New regressions reproduce strict compilation failures on the parent and pass on net8.0, net10.0 and net472. Full solution, legacy suites and Framework runner pass; modern full runners retain only the existing nullable MultiplyChecked parity failures. Measured working controls emit identical IL and retain invocation allocations. Single-extension compilation adds a 120-byte lazy cache; ordinary controls retain compilation allocations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant