Skip to content

[NOMERGE] Attempt to simplify slice bounds - #136739

Draft
NinoFloris wants to merge 8 commits into
dotnet:mainfrom
NinoFloris:perf/simplify-slice-bounds
Draft

NinoFloris wants to merge 8 commits into
dotnet:mainfrom
NinoFloris:perf/simplify-slice-bounds

Conversation

@NinoFloris

Copy link
Copy Markdown
Contributor

Based on #136738

mostly wondering about diffs.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 30, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

@NinoFloris

Copy link
Copy Markdown
Contributor Author

@EgorBo diffs with the ifdefs removed stay identical to the positive diffs in #136738 (https://dev.azure.com/dnceng-public/public/_build/results?buildId=1656969&view=ms.vss-build-web.run-extensions-tab)

We can make the switch.

@EgorBo

EgorBo commented Oct 31, 2026

Copy link
Copy Markdown
Member

@EgorBo diffs with the ifdefs removed stay identical to the positive diffs in #136738 (https://dev.azure.com/dnceng-public/public/_build/results?buildId=1656969&view=ms.vss-build-web.run-extensions-tab)

We can make the switch.

SPMI doesn't show diffs for changes in C#

@EgorBo

EgorBo commented Oct 31, 2026

Copy link
Copy Markdown
Member

@MihuBot

@NinoFloris

NinoFloris commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

@EgorBo diffs with the ifdefs removed stay identical to the positive diffs in #136738 (https://dev.azure.com/dnceng-public/public/_build/results?buildId=1656969&view=ms.vss-build-web.run-extensions-tab)
We can make the switch.

SPMI doesn't show diffs for changes in C#

Ah right... How often do the collections get refreshed again? (just for my understanding when it would show up in the perf tracking even, if this was merged hypothetically)

@EgorBo

EgorBo commented Oct 31, 2026

Copy link
Copy Markdown
Member

@EgorBo diffs with the ifdefs removed stay identical to the positive diffs in #136738 (https://dev.azure.com/dnceng-public/public/_build/results?buildId=1656969&view=ms.vss-build-web.run-extensions-tab)
We can make the switch.

SPMI doesn't show diffs for changes in C#

Ah right... How often do the collections get refreshed again? (just for my understanding when it would show up in the perf tracking even, if this was merged hypothetically)

I don't think that C# change can be merged, I expect quite a lot of regressions from that still

@NinoFloris

Copy link
Copy Markdown
Contributor Author

It did look good locally. I did do some jitdisasms and a few fresh PMI runs against ~100k methods (corelib and some libraries).

@EgorBo

EgorBo commented Oct 31, 2026

Copy link
Copy Markdown
Member

Looking at the diffs - nothing changed since the last time I tried, still quite a lot of regressions (52 improved, 42 regressed). This needs an optimization that recoveres the hack as the last step, it's not straighforward - I wouldn't rely on AI to implement it.

Reduce unused instance FIELD_ADDR and ARR_LENGTH nodes to their receiver
null checks during side-effect extraction, sharing the existing GetType
handling and preserving operand evaluation.

This lets local morph discard checks on known local addresses before they
cause address exposure. Add Slice bounds-check and unused-length codegen
coverage, along with null-exception tests.
Use the existing subtraction-based checks on all architectures for Span and Memory construction and slicing, Memory span access, string span and memory conversion, Substring, and TryGetSpan. Align ValueListBuilder.AppendSpan with the same guard so that its subsequent Slice can reuse the bounds facts.
CSE can give a copy source a new conservative value number while existing
copies retain their old identities. Follow same-typed SSA copies when checking
non-null assertions, including explicit comparisons against null.

Preserve existing VNs so comparisons and range assertions remain consistent.
Add coverage for a captured List.Count followed by an inlined Add, where
rewriting the copy VN previously prevented bounds-check elimination.
Treat address-exposed local reads as memory-load candidates, using the existing byref-exposed memory value numbers and CSE availability checks. Capture repeated reads in SSA temporaries so subsequent assertions can reuse their values.
Use the existing tree splitter to evaluate the first comparison's operands before combining the tests. This exposes pure comparisons when CSE embeds a definition in the first test, while retaining the second test's side-effect and cost restrictions.
@NinoFloris
NinoFloris force-pushed the perf/simplify-slice-bounds branch from 35ff71f to a7882c2 Compare October 31, 2026 19:23
@NinoFloris

NinoFloris commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

I should have used more local PMI coverage to diff against before claiming it was OK, but it looks worse than it is. My late night mistake.

I have pushed a few more changes, explained below. With these changes, my local ARM64 PMI run shows 4,078 smaller methods versus 2,833 larger, with roughly 31 KB less code overall. About 92% of regressed methods grow by at most 16 bytes, mostly in cases where both bounds comparisons survive in the generated code.

@EgorBo what cost/benefit threshold would you use for this change? Do you consider JIT support for transforming checks back into a widened addition a prerequisite for merging? Transforming the checks into the widened-add form in a later phase wouldn’t undo the effects of suboptimal handling in earlier phases, so we'd need everything then.

First off, a missed check in StringBuilder.Append, which needs to match the new Slice check. A substantial source of the chunkier code size regressions. Once synced, the sizes for regressions in e.g. System.Diagnostics.Debug+AssertInterpolatedStringHandler:AppendFormatted improve to the same size or smaller than before.

// Use the same check as Span<T>.Slice for 64-bit so it can be folded
// Since repeatCount can't be negative, there's no risk for it to overflow on 32 bit
if (((nuint)(uint)chunkLength + (nuint)(uint)repeatCount) <= (nuint)(uint)chunkChars.Length)
{
chunkChars.AsSpan(chunkLength, repeatCount).Fill(value);

After investigating the regressions, most fall into these categories:

  • Heuristic cutoffs: string.Substring grows from seven to eight basic blocks due to the split comparison, going over the BB cutoff. A few string.AsMemory(int, int) inline rejections for the same reasons. Also one or two finally cloning cutoffs in methods, again due to the split check.

  • Constant arguments: the widened check folds directly to one comparison, while the subtraction-based form retains two. For nonnegative constants start and count whose sum fits in an int, (uint)length >= start && (uint)(length - start) >= count can be folded to (uint)length >= start + count, but main currently lacks this boolean optimization.

  • Repeated length loads: the second load of the string, array, or span length exposed quite some limitations in CSE:

    • Partly fixed by Canonicalize unused field addresses and length reads #136738.
    • Another issue is that CSE can update a copy’s source while the copy retains its old VN, preventing later optimizations from connecting them. We now follow SSA copy definitions when proving values non-null, preserving existing VNs and assertions. This removes a few more redundant null checks on copied receivers.
    • CSE rejected direct reads of address-exposed locals as candidates. Allowing them through lets repeated Span length loads share a captured value, enabling more bounds check elimination.
      I first tried to work around these issues by reading the length into a local in C#, but that then caused regressions on inlining budgets and such everywhere, so it was reverted.

Two more CSE related issues surfaced only after those initial CSE fixes:

  • Conditional compares: Once CSE could reuse the repeated length loads, it could embed a temporary assignment inside the first comparison, blocking conditional compare formation (ccmp on ARM64). Separating that assignment from the comparison preserves evaluation order and allows the checks to be combined.

  • Exact-type assertions: Once CSE could reuse reads of address-exposed locals, it could give the captured object a new VN while the cached method-table VN still referred to the old identity. This stopped exact-type assertions from matching, leaving redundant type checks and unboxing fallbacks.

    • We now follow the CSE temporary’s defining load when matching those assertions to remove the redundant checks.
    • Alternatively, CSE could keep the affected method-table VNs and their dependent uses consistent with the new receiver identity.
      I picked the first but I’m not sure which approach is preferred here.

I have included all these opts into this PR for diff verification only.

CSE can change a method-table load's receiver VN while the cached load VN
still describes the original receiver. Consult the receiver of the defining
load as well when matching exact-type assertions, preserving existing VNs.

This removes redundant type checks and unboxing fallbacks after successful
pattern tests on captured address-exposed locals. Add codegen coverage and
verify that replacing the boxed value still requires a checked unbox.
@NinoFloris
NinoFloris force-pushed the perf/simplify-slice-bounds branch from a7882c2 to 17d43d5 Compare October 31, 2026 19:36
@NinoFloris

Copy link
Copy Markdown
Contributor Author

@MihuBot

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Runtime community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants