Conversation
Recognize unsigned carry and borrow idioms expressed through ordinary arithmetic without adding managed APIs. Lower eligible straight-line and counted-loop chains to ADD/ADC and SUB/SBB on x64 and ADDS/ADCS and SUBS/SBCS on ARM64. Preserve flags across loop backedges and materialize carry or borrow only where needed. Recognize full-width multiply-accumulate chains. Add ADX feature detection and select MULX/ADCX/ADOX for proven products on supported x64 targets; use MUL/UMULH with carry arithmetic on ARM64. Keep unrelated BigMul nodes on their existing path. Preserve managed byref tracking and fallback paths, and refine register constraints, operand reuse, addressing and loop control to avoid redundant carry spills and loads. Seed ADX without a temporary register and preserve both carry flags through LEA countdowns and JRCXZ. Use ordinary DEC selection for loops that need to preserve CF alone. Combine complementary constant shifts into SHRD or EXTR. Reuse the hardware remainder for eligible xarch quotient/remainder expressions while preserving exception behavior and observable destination values. Scope fixed-register propagation to methods with carry arithmetic and apply conservative profitability guards. Retain existing select lowering and zero-flag INC/DEC optimizations when carry transforms would add overhead. Expose these patterns in Int128, UInt128, Decimal, IEEE decimal helpers and BigIntegerCalculator. Use matching span bounds for limb loops, retain MulAdd1 unrolling, and express Montgomery reduction as a scalar carry loop. Preserve architecture-specific alternatives and 32-bit arithmetic paths. Use scalar long/ulong Tensor DivRem on x64 to retain the hardware remainder. Document the source idioms, carry-width requirements and internal helper preconditions. Widening division requires a quotient that fits in one limb; span slicing validates destination capacity before entering limb loops. Prove deleted carry-local values dead from current control flow instead of relying on liveness computed before lowering. Include exception successors and promoted-parent aliases, exclude unsupported implicit uses, and bound analysis work. Use per-query visited bitsets without caching across IR or control-flow mutations. Skip carry-folding searches until a SETCC may be available while keeping comparison recognition enabled. Share local-use, loop-entry, dead-store and address-rewrite helpers. Preserve memory ordering, flags consumers, LIR use ordering, value lifetimes and control-flow analysis validity, and require distinct loop exits. Add correctness and disassembly coverage for carry/borrow chains, wide products, decimal rounding, division and funnel shifts. Cover boundary and randomized inputs, struct and field aliases, calls, exceptions, volatile accesses, arbitrary carry inputs, generic helper expansion and exception- handler observations. Validation: - Windows x64 and all cross-target Checked JIT builds; Linux ARM64 Checked runtime and JIT builds. - x64 arithmetic suites with expensive JIT checks, register stress, AVX2 disabled and hardware intrinsics disabled. - Docker/QEMU ARM64 arithmetic suites with expensive checks, register stress and hardware intrinsics disabled; targeted x64/ARM64 disassembly checks. - Actual BigInteger MulAdd1 checks cover 4,000 random, boundary and overlap cases per run. Its x64 implementation is 344 bytes and 98 instructions, including the ADX loop and MULX scalar tail. - IEEE Decimal32/64/128 differential checks cover 31,500 arithmetic results, matching the comparison baseline byte-for-byte, including under stress. - Full Windows x64 SuperPMI validation: 980,044 contexts across nine overlapping collections, 218 known missing contexts and zero other failures. Incremental assembly comparisons found no code-size regressions; the final comparison improved one Decimal128 method by nine bytes. - Changed-line formatting and git diff --check passed. Compilation replay and code size do not establish execution equivalence or throughput gains. No full assembly comparison against the parent was rerun for the final tree, and no ARM64 hardware timing or full ARM64 SuperPMI sweep was performed.
|
Azure Pipelines: Successfully started running 8 pipeline(s). 8 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging @dotnet/jit-contrib for JIT-EE GUID update |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Pull request overview
This PR expands JIT/codegen and framework support for wide/carry-based arithmetic (including ADX on x86/x64), adds targeted lowering/LSRA/emitter updates, and introduces multiple JIT regression tests validating code shape and correctness.
Changes:
- Add new JIT opt tests covering carry/borrow chains, wide arithmetic, funnel-shift patterns, division/remainder behaviors, and decimal/biginteger helper paths
- Introduce ADX feature detection and instruction set plumbing (minipal → CoreCLR → R2R/tooling)
- Refine JIT lowering/codegen/LSRA/emitter behavior for carry arithmetic, funnel shifts, remainder reuse, and map-38 legacy encodings
Reviewed changes
Copilot reviewed 62 out of 63 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/JIT/opt/Add/WideArithmetic.csproj | Adds new JIT test project configuration w/ disasm checking |
| src/tests/JIT/opt/Add/WideArithmetic.cs | Adds wide arithmetic correctness + codegen-shape tests (128-bit, carry/borrow) |
| src/tests/JIT/opt/Add/FunnelShift.csproj | Adds new JIT test project configuration w/ disasm checking |
| src/tests/JIT/opt/Add/FunnelShift.cs | Adds funnel-shift pattern tests for x64/arm64 codegen |
| src/tests/JIT/opt/Add/Division.csproj | Adds new JIT test project configuration w/ disasm checking |
| src/tests/JIT/opt/Add/Division.cs | Adds division/remainder and decimal/biginteger helper coverage tests |
| src/tests/JIT/opt/Add/DecimalWide.csproj | Adds new JIT test project configuration for decimal wide helpers |
| src/tests/JIT/opt/Add/DecimalWide.cs | Adds reflection-based tests for internal wide decimal helpers |
| src/tests/JIT/opt/Add/DecimalMultiply.csproj | Adds new JIT test project configuration for decimal multiply |
| src/tests/JIT/opt/Add/DecimalMultiply.cs | Adds decimal multiplication oracle + overflow tests |
| src/tests/JIT/opt/Add/CarryChains.csproj | Adds new JIT test project configuration for carry-chain loops |
| src/tests/JIT/opt/Add/CarryChains.cs | Adds multi-limb carry chain tests emphasizing flags/carry lifetimes |
| src/tests/JIT/opt/Add/Carry.csproj | Adds new JIT test project configuration w/ disasm checking |
| src/tests/JIT/opt/Add/Carry.cs | Adds extensive carry-chain + muladd loop tests + new patterns (funnel shift, etc.) |
| src/tests/JIT/opt/Add/Borrow.csproj | Adds new JIT test project configuration w/ disasm checking |
| src/tests/JIT/opt/Add/Borrow.cs | Adds borrow-chain tests + mul/sub sequences and codegen constraints |
| src/tests/JIT/opt/Add/ArithmeticCodegen.csproj | Adds new JIT test project configuration |
| src/tests/JIT/opt/Add/ArithmeticCodegen.cs | Adds codegen-oriented tests for mul/div remainder, carry across blocks, etc. |
| src/native/minipal/cpufeatures.h | Adds ADX CPU feature flag constant |
| src/native/minipal/cpufeatures.c | Detects ADX via CPUID leaf 7 EBX bit 19 |
| src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.PowMod.cs | Reworks Montgomery reduce inner loop shape for bounds/carry optimizations |
| src/libraries/System.Private.CoreLib/src/System/UIntPtr.cs | Marks UIntPtr.BigMul AggressiveInlining |
| src/libraries/System.Private.CoreLib/src/System/UInt128.cs | Tweaks DivRem/BigMul for carry/borrow codegen shaping |
| src/libraries/System.Private.CoreLib/src/System/Numerics/BigIntegerCalculator.Shared.cs | Improves limb add/sub and DivRem; reshapes MulAdd1 loop; adds bounds proofs |
| src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs | Adds UInt128-specialized wide multiply/add/sub and widening-divide fast paths |
| src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs | Fixes/clarifies heuristic division correction comments |
| src/libraries/System.Private.CoreLib/src/System/Int128.cs | Adds 64-bit DivRem fast path using UInt128.DivRem and updates % operator |
| src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs | Adds widening-divide helper and reshapes decimal division/mul/rounding paths |
| src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.DivRem.cs | Disables vectorization for x64 long/ulong to preserve scalar residue behavior |
| src/coreclr/vm/codeman.cpp | Enables InstructionSet_ADX when detected & config allows |
| src/coreclr/tools/Common/JitInterface/ThunkGenerator/InstructionSetDesc.txt | Adds ADX instruction set entries and bumps NEXT_AVAILABLE_R2R_BIT |
| src/coreclr/tools/Common/JitInterface/CorInfoInstructionSet.cs | Adds ADX to instruction-set enums and supported set lists |
| src/coreclr/tools/Common/Internal/Runtime/ReadyToRunInstructionSetHelper.cs | Maps JIT instruction set ADX to R2R instruction set |
| src/coreclr/tools/Common/Internal/Runtime/ReadyToRunInstructionSet.cs | Adds R2R enum value for Adx |
| src/coreclr/tools/Common/Compiler/HardwareIntrinsicHelpers.cs | Adds ADX flag mapping in crossgen tooling |
| src/coreclr/jit/lsraxarch.cpp | LSRA changes for ADX nodes, div/rem pair physreg capture, funnel-shift reg liveness |
| src/coreclr/jit/lsrabuild.cpp | Prevents def reassign for div/rem pair; adds fixed refs for propagated single-reg defs |
| src/coreclr/jit/lsraarm64.cpp | Marks carry ops as flags consumers for allocation decisions |
| src/coreclr/jit/lsra.h | Adds carry-arithmetic state tracking + fixed-def tracking |
| src/coreclr/jit/lsra.cpp | Critical-edge handling for JCMP; selection tweaks when carry arithmetic introduces fixed refs |
| src/coreclr/jit/lowerxarch.cpp | Adds TryLowerDivRem and funnel-shift containment call site |
| src/coreclr/jit/lowerarmarch.cpp | Adds funnel-shift containment + carry-op containment of 0 on ARM64 |
| src/coreclr/jit/lower.h | Declares carry-chain lowering pipeline, div/rem reuse, and funnel-shift containment |
| src/coreclr/jit/liveness.cpp | Ensures div/rem pair division isn’t removed; null-guards prev flag clearing; adds ADX seed handling |
| src/coreclr/jit/instrsxarch.h | Adds ADCX/ADOX and JRCXZ encodings |
| src/coreclr/jit/inductionvariableopts.cpp | Adds carry-loop-aware countdown heuristics + folds four-limb offsets |
| src/coreclr/jit/hwintrinsiccodegenxarch.cpp | Adds MULX codegen path for matched multiply-carry patterns |
| src/coreclr/jit/hwintrinsic.cpp | Adds ADX slot to ISA range table |
| src/coreclr/jit/gtlist.h | Adds new GT nodes for carry/borrow/ADX chain representation |
| src/coreclr/jit/gentree.h | Adds funnel-shift/divrem-pair helpers and flag-consumption tracking |
| src/coreclr/jit/gentree.cpp | Implements IsFunnelShift and updates use-edge iteration / node display for new GT nodes |
| src/coreclr/jit/emitxarch.h | Adds helper to classify legacy map-38 instructions |
| src/coreclr/jit/emitxarch.cpp | Adjusts encoding/size/output paths for JRCXZ and legacy map-38 instructions |
| src/coreclr/jit/emitarm64.cpp | Allows ZR as the third operand for adc/sbc class instructions |
| src/coreclr/jit/compiler.h | Declares optFoldFourLimbOffsets hook |
| src/coreclr/jit/codegenxarch.cpp | Adds funnel-shift codegen, carry op support, ADX drain/seed, JCMP jrcxz lowering |
| src/coreclr/jit/codegenarmarch.cpp | Enables codegen dispatch for carry/borrow ops on ARM64 |
| src/coreclr/jit/codegenarm64.cpp | Implements carry/borrow ops with adc/sbc/cinc and funnel-shift via extr |
| src/coreclr/inc/readytoruninstructionset.h | Adds READYTORUN_INSTRUCTION_Adx |
| src/coreclr/inc/jiteeversionguid.h | Updates JIT/EE version GUID |
| src/coreclr/inc/corinfoinstructionset.h | Adds ADX to CORINFO_InstructionSet + string/R2R mappings |
| src/coreclr/inc/clrconfigvalues.h | Adds EXTERNAL_EnableADX config knob |
Suppressed comments (4)
src/coreclr/jit/codegenxarch.cpp:1
- The funnel-shift codegen path will generate incorrect code if LSRA assigns
targetReg == hiwhilelois in a different register (themov targetReg, lowould clobberhibeforeshrdconsumes it). The currentassert(targetReg != hi)prevents this in debug, but release builds would still be at risk if the allocation happens. Consider making the codegen robust by handlingtargetReg == hi(e.g., emit the equivalentshldform with the complementary count, or ensure the destination is forced/preferred toloand guaranteed not to pickhi), rather than relying on an assertion.
// Licensed to the .NET Foundation under one or more agreements.
src/coreclr/tools/Common/JitInterface/CorInfoInstructionSet.cs:1
- The
InstructionSetInfoentries foradxprovide an empty managed name (second argument). If this field is used by tooling/diagnostics (e.g., for display, option names, or managed wrapper association), leaving it empty can cause confusing output or missing metadata. Consider supplying an appropriate name (or adding a comment explaining why ADX intentionally has no managed-facing name).
src/coreclr/tools/Common/JitInterface/CorInfoInstructionSet.cs:1 - The
InstructionSetInfoentries foradxprovide an empty managed name (second argument). If this field is used by tooling/diagnostics (e.g., for display, option names, or managed wrapper association), leaving it empty can cause confusing output or missing metadata. Consider supplying an appropriate name (or adding a comment explaining why ADX intentionally has no managed-facing name).
src/coreclr/jit/gentree.cpp:1 GenTree::IsFunnelShift()currently classifies any containedOR(RSZ, LSH)as a funnel shift without validating that the shift counts are immediate constants (and within the legal range forextron ARM64) or that they are the complementary counts expected by the codegen paths. Since xarch/arm64 codegen assumes an immediate (AsIntCon()->IconValue()), tightening this predicate (e.g., requiring constant shift counts and/or a canonicalized form) would make the invariant explicit and reduce the risk of assertion failures or invalid immediates.
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/tests/JIT/opt/Add/WideArithmetic.cs:1
- This new test file is missing the standard .NET Foundation MIT license header present in other newly added tests (e.g., FunnelShift.cs, Division.cs). Add the usual header block at the top of the file to match repository conventions.
src/tests/JIT/opt/Add/Borrow.cs:1 - This new test file is missing the standard .NET Foundation MIT license header used across the repo. Add the header comment block at the top of the file for consistency and compliance.
src/tests/JIT/opt/Add/DecimalMultiply.cs:1 - This new test file is missing the standard .NET Foundation MIT license header. Other new tests in this PR include it; please add the same header here.
src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs:1 - The remainder computation relies on modulo-2^32 wraparound (
low - quotient * den) rather than directly expressingdividend - quotient * den. While this works given the method’s contract (remainder fits in 32 bits), it’s non-obvious and easy to misread as a bug. Consider computing the remainder fromdividendexplicitly (e.g., using 64-bit arithmetic and casting at the end) or add an explanatory comment tying correctness to the invariants (high < den, remainder < den < 2^32).
|
The API proposal variants that return both the value and carry probably shouldnt be closed since there are usecases that need both, like with divrem. |
There was a problem hiding this comment.
🔵 Needs a closer look
The changes span core JIT lowering/LSRA/emitter plus ISA plumbing and numerics codegen-shaping, which warrants final human review despite only minor actionable nits found.
Review details
- Files reviewed: 62/63 changed files
- Comments generated: 2
- Review effort level: Lite
| // SCEV's general trip-count materialization currently handles only unit | ||
| // strides. Recognize the bounded four-lane case: i starts at zero, steps | ||
| // by four, and continues while i < length - 3 for a nonnegative length. | ||
| // Its trip count is length / 4; the induction variable cannot overflow. |
| prod1 = bufDen.Low64; | ||
| do | ||
| { | ||
| quo--; | ||
| num += prod1; | ||
| high += den; | ||
| high += (num < prod1) ? 1L : 0L; | ||
| } while (high < 0); |
|
This is another massive PR that likely needs to be split up and done incrementally (there's at least 3, if not 4+, different PRs here) I'd note I have my own local branch that is handling some adc/sbb lightup as well, which went a bit of a different route |
cmpandtest. #76502's redundant comparisons; multi-consumer flag reuse remains.Summary
Recognize wide-integer arithmetic expressed in ordinary C# and use hardware carry, borrow, full-width multiplication and division results. No new public API is required.
x - (x / y) * yexpressions on xarch.The matchers conservatively reject unsupported aliasing, intervening flag clobbers and unsafe movement across calls or exceptions. Carry-local deadness is proved against current control flow, including exception successors, with bounded analysis. Existing fallbacks and architecture-specific 32-bit paths remain available. Register-pressure and consumer-shape guards limit extra moves, spills and flag materialization.
Assembly examples
The excerpts below focus on the arithmetic; unrelated setup, prologues and epilogues are omitted. Register allocation and calling conventions vary by target.
Carry and borrow
For
count += (a + b < a) ? 1UL : 0UL, the x64 arithmetic changes from:The complete Count64 test method shrinks from 22 to 14 bytes. UInt128 subtraction changes from 40 to 25 bytes on Windows x64:
The corresponding ARM64 arithmetic uses hardware carry/borrow directly:
Reusing subtraction flags in String.IndexOf
In
String.IndexOf(char, int), the subtraction computingLength - startIndexsupplies the borrow flag for argument validation, eliminating a separate comparison:The remaining length also stays in the search helper's argument register, removing a move before each of the two call paths. The search implementation is unchanged.
BigInteger multiply-accumulate
Actual
BigIntegerCalculator.MulAdd1on Windows x64 shrinks from 589 bytes / 178 instructions to 344 bytes / 98 instructions. The baseline repeatedly materializes carry with CMP/SETB/MOVZX and spills its running high word. The new four-limb main loop is:CF and OF are seeded before entry, survive the backedge, and are drained on exit. The loop has no carry spills or per-limb bounds checks. LEA preserves both flags; DEC would destroy OF. JRCXZ plus JMP costs two branch instructions per iteration but preserves both chains. Bounds validation and the scalar tail remain outside this excerpt.
On ARM64, the actual rebuilt helper is 368 bytes / 100 instructions. Its first limb uses:
The next limb consumes the high word and carry; the final carry also flows into the next loop iteration. This code was executed under local Docker/QEMU ARM64 emulation, including 4,000 random, boundary and overlap cases.
Reusing the hardware remainder
For
q = x / y; r = x - q * y; return q + r, the Windows x64 test shrinks from 22 to 15 bytes:This recognizes remainder reconstruction. General pairing of separately expressed
/and%is not part of this change.Benchmarks
Fresh measurements compare parent
76e6281d52fwithaf9cdfc2c6eon an AMD Ryzen 9 9950X, Windows x64, pinned to logical CPU 6. Results are medians from nine fresh-process samples per version, alternating before/after order. Tiering and ReadyToRun are disabled; workstation GC is used. There were no concurrent builds or replays during timing.Both versions use the same Checked runtime/CoreLib/support framework and separately compiled Release Numerics assemblies from their respective source trees, with the respective Checked JITs. These isolate the JIT + Numerics changes; they are not a comparison of two complete framework distributions or a measurement of the CoreLib Decimal/Int128 rewrites. API compatibility validation was disabled for these isolated benchmark assemblies. Setup, warmup, reflection/delegate creation and explicit collections are outside timing. Checked-runtime overhead remains, including in allocating public operations.
A limb is 64 bits. Public add/subtract/multiply/square use deterministic positive inputs of the stated width; division uses a roughly twice-as-wide dividend. Public result allocation is included. Private helpers use reusable buffers and include delegate overhead. All before/after result digests match, and allocation counts match for every main-harness case. Private multiply helpers allocate nothing.
Representative results (ns/op; speedup = before / after):
Four-limb subtraction is approximately flat (1.3% higher median); the larger tested cases improve. Small differences should not be read as statistically established changes. These are warm-cache measurements on one AMD CPU, with no Intel or native ARM64 throughput claim.
Montgomery
Reduceincludes restoring the input buffer and the private-method delegate call.ModPow65537uses exponent 65,537;ModPowDenseuses 2^256 - 189. Their timings include the complete public operation and allocation. All Montgomery and ModPow result digests match between versions.All measured cases
All samples, including outliers, are retained in the local measurement artifacts. No samples were discarded. The main harness targets approximately 80 ms per case using parent-calibrated iteration counts shared by both versions; the Montgomery harness uses the same approach.
SuperPMI
Full Windows x64 assembly comparison against the PR parent across all nine locally available collections:
The cleaned corpus replays with zero compilation failures and zero missing contexts for both JITs. The original input contains 980,044 contexts; 218 cannot replay against the parent because recorded runtime answers are missing. Those baseline-incomplete records were excluded using the standard
mcs -stripcleanup workflow, with their identities retained. No candidate-only failure was excluded. Original collections are unchanged. Collections overlap, so counts and byte totals are per replay context, not unique methods or application-size savings.Changed contexts shrink from 354,067 to 336,351 generated code bytes (17,716 bytes / 5.00%). This percentage applies only to changed contexts. The six increases total 10 bytes, with a largest increase of four bytes: Int128 conversion/register-allocation tradeoffs and four one-byte differences in dependency-injection helpers. They are not arithmetic compilation failures. Code size alone does not establish throughput.
The parent JIT was rebuilt with only the JIT/EE GUID retargeted to replay this corpus. The ISA addition appends ADX without renumbering existing entries; baseline optimization sources are unchanged. SuperPMI replays the recorded IL and therefore complements, rather than replaces, tests and benchmarks of the modified library sources.
Validation
ARM64 execution and assembly are validated through emulation; native ARM64 timing and a full ARM64 SuperPMI sweep have not been performed. Replay checks compilation and JIT assertions, not runtime arithmetic or GC correctness by itself.