Conversation
Physical promotion does not currently charge for decomposing overlapping whole-struct stores. Account for the extra moves when a vector-sized struct is split entirely into sub-native fields, with more distinct primitive access entries than native-sized copy parts. Reuse existing store counters and register-move costs, amortize the original vector move across fields, and exclude call-result readbacks already costed separately. This is a per- field approximation, not joint evaluation of the final replacement set. Validation against this change's base on updated main a652cdf: - Windows x64 Checked and Release and Linux x64 Release JIT builds. - 18 existing physical-promotion regression runs pass with normal, forced promotion and JitStress=2 settings. - Existing promotion facts pass on both JITs with tiering/PGO enabled. - Eleven SuperPMI collections: 983,234 comparable contexts; 238,457,491 -> 238,457,477 bytes (14 saved). 2 shrink; 2 grow by 32 bytes total. Zero compilation failures; 190 missing-recording contexts excluded. The 190 recording gaps affect both sides. - Release PIN JIT instructions, benchmarks: +0.00629%. - Release PIN JIT instructions, HashSet PGO: +0.00266%. Standalone changes are two 23-byte Markdig reductions and two 16-byte StringBuilder increases; all four have higher static PerfScore. With address propagation enabled, the controlled HashSet comparison saves 1,174 bytes; the largest method is 2,878 -> 2,268 bytes with a lower PerfScore. Distinct access entries approximate the final replacement set. Code size, static PerfScore and JIT instruction counts do not establish application execution speed. No ARM64 or x86 execution is claimed. Related to dotnet#133833.
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 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 subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
🔵 Needs a closer look
Resolve the potential double-counting of vector-copy costs and clarify the return-contract documentation.
Pull request overview
This PR improves RyuJIT physical-promotion costing for fragmented vector-sized struct copies.
Changes:
- Tracks store accesses in release builds.
- Accounts for additional register moves when vector copies are fragmented.
- Updates related return-contract documentation.
File summaries
| File | Reviewed changes | Findings |
|---|---|---|
src/coreclr/jit/promotion.cpp |
Adds vector-copy cost accounting and access tracking. | Moderate (1 vote): A physical vector copy may be charged twice when both locals are promoted. Nit (1 vote): The return-contract comment should describe all FIELD_LIST construction failures. |
Review details
Suppressed comments (2)
src/coreclr/jit/promotion.cpp:750
- A single
STORE_LCL_VAR dst, srcis recorded once asCountStoreSourceonsrcand again asCountStoreDestinationondst. If both locals are promoted into sub-native fields, each side's independentEvaluateReplacementcalls charge the same physical vector copy, so a 16-byte copy split into four 4-byte fields is penalized twice (roughly six extra moves instead of the three added by the decomposition). This can reject profitable promotions based on double-counted cost; please deduplicate this per copy or document why the two-sided charge is intentional.
countVectorCopies += otherAccess.CountStoreSource + otherAccess.CountStoreDestination +
otherAccess.CountPassedAsRetbuf - otherAccess.CountStoredFromCall;
countVectorCopiesWtd += otherAccess.CountStoreSourceWtd + otherAccess.CountStoreDestinationWtd +
otherAccess.CountPassedAsRetbufWtd - otherAccess.CountStoredFromCallWtd;
src/coreclr/jit/promotion.cpp:2441
- This contract is narrower than the implementation:
falseis also returned when the argument is not found, the ABI requires a stack/by-reference argument, or the field-list construction rejects a remainder or partial overlap. Please describe the general failure to form a FIELD_LIST rather than attributing every failure to write-backs.
// True if the argument was replaced; false if write-backs are required.
- Files reviewed: 1/1 changed files
- Comments generated: 0
- Review effort level: Lite
Record whole-local copies that induce matching accesses from already selected replacements. Exclude those copies from the induced field fragmentation charge so the second endpoint does not pay again for an existing decomposition. Keep one-sided and partial-copy costs. Reuse the existing induced-promotion retry pass and its access records. Add SharedVectorCopyCost with behavioral and Windows x64 codegen coverage. It restores main codegen: 340 to 302 bytes, 86 to 81 instructions and 96 to 80 local frame bytes versus the previous model. Restore the existing FIELD_LIST return-contract comment. Validation: Windows Checked/Release and Linux Release JIT builds; focused normal, promotion, JitStress=2 and PGO runs; Windows codegen check passes and fails on the previous model; Linux execution; 18 existing regression runs. All passed. SuperPMI: 983,234 comparable contexts, no compilation failures, 190 shared recording gaps. Net 14 bytes saved, unchanged from the previous model. Controlled HashSet savings remain 1,174 bytes with address propagation. Release JIT instructions versus main increase 0.00638% on benchmarks and 0.00196% on HashSet. Application execution speed and ARM64/x86 execution were not measured.
|
Addressed the two points from this review in afaadeb. The cost model now reuses the existing induced-access retry pass. When selected replacements induce matching field accesses at the other endpoint of a whole-local copy, that copy is excluded from the second field's fragmentation charge. One-sided and partial copies retain their cost. This fixes the incremental decision; jointly selecting sets when neither endpoint qualifies initially remains outside the existing algorithm. Added SharedVectorCopyCost: the previous model promoted only one destination field; the revised model promotes all five, restoring main's codegen. It goes from 340 to 302 bytes, 86 to 81 instructions, and 96 to 80 bytes of local frame space. The new Windows x64 codegen check passes with the fix and fails on the previous model at an unwanted short-field reload. Behavioral tests pass on Windows and Linux, including Windows promotion stress, JitStress=2 and tiering/PGO. The test-project build and 18 existing regression runs also pass. Replayed 983,234 comparable SuperPMI contexts with no compilation failures (190 shared recording gaps). Corpus totals are unchanged: 14 bytes saved overall, including the two existing 16-byte StringBuilder regressions. The controlled HashSet comparison retains its 1,174-byte saving. Release JIT instruction counts versus main increase 0.00638% on benchmarks and 0.00196% on HashSet; these are compiler-work measurements, not application timings. Also restored the original FIELD_LIST return-contract comment and updated the PR description with the implementation and validation results. |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved promotion-cost accounting and test-runner gating issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/coreclr/jit/promotion.cpp:888
- The induced-copy credit is not applied when both endpoints already have direct primitive accesses.
PickPromotionsevaluates those accesses withinducedAccess == nullptr; laterInduceAccessreturns as soon as the candidate aggregate already overlaps the field, so noCountFullCopiesentry is recorded for either endpoint. The same whole-local copy is therefore charged once while evaluating each endpoint, which can reject otherwise profitable promotion. The credit needs to account for matching replacements already present in the other endpoint's aggregate, not only fields newly selected through induced promotion.
if (inducedAccess != nullptr)
{
assert(inducedAccess->CountFullCopies <= countVectorCopies);
countVectorCopies -= inducedAccess->CountFullCopies;
countVectorCopiesWtd = max(0.0, countVectorCopiesWtd - inducedAccess->CountFullCopiesWtd);
src/tests/JIT/Directed/Directed_do.csproj:18
- All neighboring physical-promotion projects are gated by
_UseOutOfProcessPhysicalPromotionTests, but this new project requires process isolation and is referenced unconditionally. On mobile targets where that property is false, the test is still added to the merged Directed runner even though the other physical-promotion projects are omitted. Apply the same condition here so it is not merged into an unsupported runner.
<MergedWrapperProjectReference Include="physicalpromotion\SharedVectorCopyCost.csproj" />
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
Add a stack-argument variant with six preceding integer arguments, reusing the existing struct and an inlined copy body. Run the assembly assertions with the X64 prefix on both Windows and Unix. Validation: Windows baseline/candidate behavioral runs in normal, forced-promotion, JitStress=2 and PGO modes; Linux optimized and PGO runs; actual Release test-project build. All pass. Assembly checks pass with the revised model and fail with the previous cost model on both platforms. For the final shared-body test, Windows code shrinks from 518 to 478 bytes (115 to 110 instructions); Linux shrinks from 441 to 409 bytes (109 to 101 instructions). Both restore main codegen. These are generated-code comparisons, not measured application throughput. Compiler code is unchanged.
|
Added cross-platform regression coverage in c514c29. SharedVectorCopyCost now includes a variant with six integer arguments before the struct, forcing stack passing on Unix x64. Both variants share the same inlined body and use X64 assembly assertions. The stack-argument check passes with the revised model and fails with the previous cost model on both Windows and Linux. In the final shared-body test, Windows improves from 518 to 478 bytes (115 to 110 instructions), and Linux from 441 to 409 bytes (109 to 101 instructions). Both restore main codegen. Behavioral tests and the Release test-project build pass; the PR description now includes separate Windows and Linux assembly excerpts. This commit changes tests only. The separate regular-promotion endpoint concern in the latest inline comment is not addressed by this update. |
There was a problem hiding this comment.
🔵 Needs a closer look
Two unresolved moderate issues affect whole-copy cost accounting and mobile test integration.
Review details
Suppressed comments (2)
src/coreclr/jit/promotion.cpp:1519
- Only the physical↔physical path sets
isFullCopyhere.InduceAccessesFromRegularlyPromotedStructstill callsInduceAccesswith the defaultfalse, so a vector-sized candidate copied to/from an already regularly promoted whole local gets matching induced fields but never subtractsCountFullCopies; its whole-copy fragmentation is charged again even though both endpoints are promoted. Compute/pass the same whole-local predicate in that path, and cover it with a regression case.
[!NOTE] This review comment was created by GitHub Copilot.
bool isFullCopy = (candOffs == 0) && (inducerOffs == 0) &&
(size == m_compiler->lvaGetDesc(candidate)->GetLayout()->GetSize()) &&
(size == m_compiler->lvaGetDesc(inducer)->GetLayout()->GetSize());
src/tests/JIT/Directed/Directed_do.csproj:18
Directed_do.csprojuses_UseOutOfProcessPhysicalPromotionTeststo reference the physical-promotion child projects only on supported targets and compiles their.cssources inline otherwise (lines 84-89). This new reference is unconditional; on mobile targets the referenced project setsDisableProjectBuild=true, butSharedVectorCopyCost.cshas no fallbackCompileentry, so the regression test does not participate in that mobile test path. Mirror the existing conditional reference/fallback pattern or explicitly mark this test unsupported on those targets.
[!NOTE] This review comment was created by GitHub Copilot.
<MergedWrapperProjectReference Include="physicalpromotion\SharedVectorCopyCost.csproj" />
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Charge vector-copy fragmentation only for fields with scalar reads, using existing access and store-destination counts. Definition-only fields can propagate into struct copies and eliminate the local. Pass whole-local copy information from regular promotion and require the classification at every InduceAccess call. Add regular-endpoint, vector array-copy and constant-Guid coverage to SharedVectorCopyCost. Register its mobile fallback through the existing Directed wrapper conditions. Validation against main a652cdf: - 983,234 comparable SuperPMI contexts in 11 Windows x64 collections: identical assembly, zero failures; 190 shared missing recordings. - Windows optimized, forced-promotion, JitStress=2 and Tier1 tests pass; Linux behavioral and codegen checks pass; 18 existing regression runs pass. - Array-copy check fails on main and passes here; constant-Guid check rejects the previous cost model and passes here. - Windows Checked/Release and Linux Release JIT builds pass. - Five target/runtime MSBuild evaluations register the test exactly once. - Focused readonly Color record: Windows FullOpts 394 -> 271 bytes, Tier1/PGO 329 -> 269; Linux FullOpts unchanged at 573 bytes. - PIN JIT instruction counts: benchmarks +0.00805%, HashSet +0.00224%. No application-speed measurement or mobile/ARM64/x86 execution is claimed.
|
Addressed both findings from review 5199750245 in f0b4607:
The cost model also preserves definition-only fields so constant propagation can eliminate their struct storage. The final comparison with main has identical assembly across 983,234 comparable SuperPMI contexts, zero compilation failures and 190 shared missing recordings. The focused readonly Color record example improves Windows FullOpts from 394 to 271 bytes and Tier1/PGO from 329 to 269; Linux FullOpts is unchanged. The PR description now includes its C# and assembly diff, with all results comparing main to the final PR. Windows optimized/stress/Tier1 tests, Linux checks, and 18 existing regression runs pass. PIN reports small increases in JIT compilation instructions (+0.00805% and +0.00224%); application execution speed has not been measured. |
There was a problem hiding this comment.
🔵 Needs a closer look
Definition-only fields may be incorrectly counted, and the regression test lacks a codegen assertion.
Review details
Suppressed comments (2)
src/coreclr/jit/promotion.cpp:742
primitiveAccessCountincludes every primitive entry, even entries whereCount == CountStoreDestinationand the field is only defined. That makes an otherwise eligible vector copy look fragmented and can reject promotion of the actually-read fields; the comment above says definition-only fields can be propagated and should not contribute to this cost. Exclude definition-only entries from both the count and the mixed-width check (for example, requireotherAccess.Count > otherAccess.CountStoreDestinationhere).
if (otherAccess.AccessType != TYP_STRUCT)
{
primitiveAccessCount++;
costVectorCopies &= genTypeSize(otherAccess.AccessType) < TARGET_POINTER_SIZE;
}
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:130
- This regression only asserts the returned value, so it would still pass if the regular-promotion endpoint were charged again and the optimizer chose the old fragmented code—the
isFullCopyaccounting changes codegen/cost decisions, not semantics. Please add a disassembly assertion (or another observable promotion check) for this method so reverting the regular-promotion path fix makes the test fail.
private static int CopyToRegularlyPromotedLocal(int input)
{
// The four-field destination is eligible for regular promotion. Whole-local
// copies must credit that endpoint just as they credit physical promotion.
ArrayValue source = CreateArrayValue(input);
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Count only primitive accesses with scalar reads when deciding whether a vector-sized copy fragments. Apply the same filter to the mixed-width gate, using the existing access and store-destination counters. Add an X64 codegen assertion for CopyToRegularlyPromotedLocal. It passes on Windows and Linux. Disabling only regular-endpoint copy credit makes the assertion fail on Windows; Linux already selects the checked shape. Validation: 983,234 comparable contexts across eleven SuperPMI collections produce identical assembly to f0b4607, with zero compilation failures and 190 shared missing recordings. Windows optimized, forced-promotion, JitStress=2 and Tier1/PGO tests pass; Linux tests and codegen checks pass. Windows Checked/Release and Linux Release JIT builds pass. The focused Color example remains 271 bytes FullOpts and 269 bytes Tier1 on Windows.
|
Addressed both findings from review 5200210443 in f9f5ff8.
The eleven-collection SuperPMI replay has identical assembly across 983,234 comparable contexts, zero compilation failures and 190 shared missing recordings. Windows optimized, forced-promotion, JitStress=2 and Tier1/PGO tests pass; Linux behavioral and codegen checks pass. The Color example remains 271 bytes FullOpts and 269 bytes Tier1 on Windows. The description has been updated with the final cost-accounting rules, cross-platform assertion coverage and refreshed PIN measurements against main. The separate load-reuse experiment is not included. |
There was a problem hiding this comment.
🔵 Needs a closer look
The JIT cost-accounting changes warrant final human review.
Review details
Suppressed comments (1)
src/coreclr/jit/promotion.cpp:1486
- The identical
isFullCopypredicate is maintained separately in this function and inInduceAccessesInCandidatebelow. Keeping the whole-local classification duplicated makes these two induction paths easy to diverge; factor it into one helper and use that helper at both call sites so the full-copy accounting cannot regress in only one path.
bool isFullCopy = (candidateOffs == 0) && (regPromOffs == 0) &&
(size == m_compiler->lvaGetDesc(candidateLcl)->GetLayout()->GetSize()) &&
(size == regPromDsc->GetLayout()->GetSize());
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Use IsFullCopy in both induction paths and reuse Compiler::IsEntireAccess for each endpoint. Preserve each caller's copy size and partial-copy classification, and remove the unused candidate offset. Validation: isolated Windows x64 Checked build; focused behavior and codegen checks in optimized, forced promotion, JitStress=2 and Tier1/PGO modes. Three SuperPMI collections have identical assembly across 87,586 matched contexts, no compilation failures and six shared recording misses. Changed-line clang-format and git diff --check pass. Linux and the full suite were not rerun for this refactor.
|
Addressed the duplicate whole-copy predicate from the review in 3ea3712. Both induction paths now use one helper, which reuses Compiler::IsEntireAccess for each endpoint and preserves partial-copy handling. The isolated Windows x64 Checked build and focused behavior/codegen checks pass, including forced promotion, JitStress=2 and Tier1/PGO. A three-collection SuperPMI comparison against the previous PR head has identical assembly across 87,586 matched contexts, no compilation failures and six shared recording misses. The PR's generated-code example is unaffected. Linux and the full suite were not rerun for this refactor. |
There was a problem hiding this comment.
🔵 Needs a closer look
Moderate findings remain in the promotion heuristic and test configuration.
Review details
Suppressed comments (4)
src/coreclr/jit/promotion.cpp:895
- This is a global promotion-decision heuristic, but the validation reports only code size, static PerfScore, and JIT-work deltas; it explicitly does not measure generated-program throughput. Because this charge can suppress field promotion for all 16-byte SIMD-layout structs, please add a representative execution benchmark or document why code-size evidence is sufficient for this tradeoff before relying on it as a performance improvement.
costWith += countVectorCopiesWtd * extraMoves * COST_REG_ACCESS_CYCLES;
src/coreclr/jit/promotion.cpp:741
- The mixed-width gate is an explicit part of this heuristic, but the added disassembly tests exercise only 4-byte and 2-byte fields on x64, so every tested primitive access remains smaller than the native word. A regression that incorrectly charges (or fails to charge) a 16-byte struct containing an 8-byte field would pass these tests; add a codegen case with a native-width access alongside smaller fields and assert the intended no-charge behavior.
costVectorCopies &= genTypeSize(otherAccess.AccessType) < TARGET_POINTER_SIZE;
src/coreclr/jit/promotion.cpp:730
EvaluateReplacementis also called for induced-only fields, wherePickInducedPromotionspasses a zero-countfakeAccess. In that case this gate is false (0 > 0), and the primitive count below scans onlym_accesses, so induced-only sub-native fields can never incur the new fragmentation cost. SinceCountFullCopiesis only subtracted after this gate, partial whole-struct copies are also left uncharged; include induced accesses in the eligibility/count calculation while retaining the full-copy credit.
bool costVectorCopies = (access.Count > access.CountStoreDestination) &&
(genTypeSize(access.AccessType) < TARGET_POINTER_SIZE) &&
varTypeIsSIMD(layout->GetRegisterType());
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.csproj:12
- This disassembly test relies on physical promotion being enabled, but the project does not set
DOTNET_JitEnablePhysicalPromotion=1. A run under a configuration or test scenario that sets this switch to 0 will skipPhysicalPromotionentirely, so the checks no longer exercise this regression (and can fail on the expected promoted-code patterns). The sibling physical-promotion projects set this variable explicitly (for example,physicalpromotion/fuzzlyn1.csproj:10); add the same environment entry here.
<CLRTestEnvironmentVariable Include="DOTNET_TieredCompilation" Value="0" />
<CLRTestEnvironmentVariable Include="DOTNET_JITMinOpts" Value="0" />
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Include induced accesses in fragmentation eligibility and field counting, reusing FindAccess to avoid counting existing scalar reads twice. Preserve full-copy credit and scan induced siblings only for eligible accesses. Exclude struct initializations from the existing destination counters so initialization does not incur a copy charge; no per-access fields are added. Add X64 coverage for a partial copy into a destination with only induced fields and for a native-width field alongside smaller fields. Explicitly enable physical promotion in the test project. Validation: Windows x64 Checked and Linux x64 Release builds; focused Windows optimized, forced-promotion, JitStress=2 and Tier1/PGO behavior; Linux behavior and both platforms' codegen checks. Negative controls fail on the prior JIT and, for mixed-width coverage, with only that guard removed. The induced-copy example shrinks 472 to 373 bytes on Windows and 503 to 367 on Linux; the mixed-width example is unchanged. All eleven SuperPMI collections: 983,234 matched contexts, identical assembly and 238,457,491 bytes on both sides, 190 shared misses and no new misses or compilation failures. MSBuild confirms process isolation and the promotion setting. Formatting, diff and ASCII checks pass. No runtime benchmark, new throughput, full-tree, x86 or ARM run is claimed.
|
Addressed the three code/test points from this review in 8f8a736:
Struct initializations are excluded from the existing destination counters used for copy costing. This avoids charging initialization as a copy and adds no per-access fields. It also preserves the console formatter's baseline assembly in the retained corpus. Windows Checked and Linux Release builds, focused behavior and codegen checks pass. Windows coverage includes forced promotion, JitStress=2 and Tier1/PGO. All eleven SuperPMI collections retain identical assembly across 983,234 matched contexts, with no compilation failures or new misses and 190 shared recording misses. The description now reflects the net cost-accounting behavior, focused coverage and current validation limits. |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings remain around initialization handling and compilation-time cost, with target-specific coverage gaps.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
src/coreclr/jit/promotion.cpp:1633
- This recognizes only integral-constant zero initialization, but the JIT also represents
initblkpatterns withGT_INIT_VAL;GenTree::IsInitVal()includes both forms, andgtNewStoreValueNodecan turn such a value into aGT_STORE_LCL_VAR. Those stores are therefore still counted asCountStoreDestinationand can be charged as vector copies, contrary to the stated exclusion for struct initialization. UseIsInitVal()here and cover a nonzeroinitblkcase.
if (lcl->TypeIs(TYP_STRUCT) && lcl->Data()->gtEffectiveVal()->IsIntegralConst())
{
flags |= AccessKindFlags::IsInit;
src/coreclr/jit/promotion.cpp:905
- This branch uses
TARGET_POINTER_SIZEto change the promotion threshold, so a 16-byte SIMD copy is charged differently on x86/ARM64 than on x64. The new codegen assertions cover only X64 (and X64-WINDOWS), while the behavioral assertions cannot detect a target-specific promotion/costing regression. Please add at least an ARM64/x86 codegen check or explain why those targets are intentionally out of scope.
[!NOTE] This review comment was created by GitHub Copilot.
unsigned nativeParts = layout->GetSize() / TARGET_POINTER_SIZE;
if (costVectorCopies && (primitiveAccessCount > nativeParts))
{
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:174
- The implementation is not x64-only:
ClassLayout::GetRegisterType()treats 16-byte non-GC layouts asTYP_SIMD16on everyFEATURE_SIMDtarget, and the threshold changes withTARGET_POINTER_SIZE. These are the only disassembly assertions in the new suite, so ARM64 and x86 executions exercise behavior without detecting a target-specific promotion or copy-shape regression. Add applicable target-specific codegen coverage (or otherwise validate the heuristic on those supported SIMD targets).
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
Use IsInitVal to exclude nonzero struct initialization from copy counts. Skip the induced-field summary when the existing full-copy count is zero. Add a nonzero initblk test and ARM64 vector-copy / x86 native-field exclusion checks, without adding cached state or per-access counters. Windows and Linux x64 behavior and FileCheck pass. Matching-source crossgen2 checks pass for Windows-target ARM64 and x86 code generation; ARM64 CopyInducedToArray improves from 448 to 380 bytes against main, and its check fails on main. x86 remains at 409 bytes, as intended. No ARM64/x86 execution is claimed. Full Windows x64 SuperPMI: 983,234 comparable contexts, identical assembly and 238,457,491 bytes on both sides; 190 shared misses, no compile failures. Release-JIT PIN counts versus main increase 0.03082% on benchmarks.run (51,617 contexts) and 0.01260% on HashSet (1,307 contexts). Formatting, diff and added-source ASCII checks pass. The full JIT tree and generated-code execution-speed benchmarks were not run.
|
Addressed the initializer and target-coverage points from this review in c1340aa.
The description includes the target coverage and current Release-JIT instruction-count measurements. I replied separately to the scan-cost thread with those measurements. |
There was a problem hiding this comment.
🔵 Needs a closer look
Moderate codegen-test coverage gaps remain and require review before approval.
Review details
Suppressed comments (3)
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:229
- This method performs eight
destination[i] = valuecopies, but the disassembly check requires only two vector load/store pairs. Because SuperFileCheck scopes these ordered checks to this method, a regression that scalarizes six of the eight array stores can still pass. Add checks for all eight vector stores (or an equivalent assertion that every assignment remains a vector copy).
// X64: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64: {{v?movups}} xmmword ptr
// X64: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64: {{v?movups}} xmmword ptr
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:217
- The ARM64 check likewise requires only one vector load and one vector store even though this method writes eight array elements. SuperFileCheck limits the match to
CopyInducedToArray, so it does not prove that the remaining seven stores were not scalarized; add enough target-appropriate checks to cover all eight assignments.
// ARM64: ldr q{{[0-9]+}}, [{{fp|sp}}
// ARM64: {{stp|str}} q{{[0-9]+}},
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:174
- The direct full-copy case has disassembly assertions only under
X64-WINDOWS, so ARM64 can regress from vector array stores to scalar stores while the behavioral checks still pass. The new cost path is applicable to ARM64 as well; add ARM64-specific checks forCopyToArray(or a separate ARM64 codegen test).
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmm{{[0-9]+}}, xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Check all eight induced array copies on x64 and ARM64 and cover direct array copies on ARM64. Bound store checks at the first return so fallback copies cannot mask missing fast-path stores, without requiring repeated source loads. SuperFileCheck passes against retained Windows/Linux x64 and ARM64 PR output; x86 checks remain passing. All 26 individual fast-path store-removal mutations fail, and ARM64 main fails the vector-store checks. Only test directives changed; no native ARM64 execution is claimed.
|
Addressed the three coverage points in 36ea665. SuperFileCheck passes against the retained Windows/Linux x64 and ARM64 PR assembly, with x86 checks also passing. Removing each fast-path vector store individually makes all 26 mutation cases fail; ARM64 main fails the vector-store checks. This is test-directive-only validation against retained assembly, not a new ARM64 execution run. The PR description now reflects this coverage. |
There was a problem hiding this comment.
🔵 Needs a closer look
Address the two moderate test assertion and X86 coverage issues.
Review details
Suppressed comments (2)
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:177
- The direct four-int
ArrayValuepath has no X86 disassembly assertion. Since this heuristic deliberately depends onTARGET_POINTER_SIZE, x86 is the platform where these four native-sized fields must remain uncharged; the X86 check later inCopyInducedToArraycovers only the induced-access path. Add an X86 no-vector check here so a regression in the regular-access path is caught as well.
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: call {{.*}}SharedVectorCopyCost:CheckFields
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS-LABEL: {{^ *ret}}
src/tests/JIT/Directed/physicalpromotion/SharedVectorCopyCost.cs:176
- These patterns match loads and stores alike: x64 prints both as
vmovups xmmword ptr .... The method can therefore satisfy these four checks with source reloads while still emitting scalar stores to the array, so the check would not catch the regression this test is intended to guard. Match the store operand (for example,xmmword ptr [...], xmmN) or otherwise anchor each check to the destination store.
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
// X64-WINDOWS: {{v?movups}} xmmword ptr
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Physical promotion can split a vector-sized struct copy into many scalar stores without charging for that extra work. Account for the additional moves when promoting a struct entirely into sub-native fields, preserving vector copies when field promotion is not profitable.
Split from #133833 at @jakobbotsch's request so this change can be reviewed independently.
All comparisons below are from base
mainata652cdfed564to this PR.Focused generated code
A drawing command supplies an RGBA color as four little-endian floats. Validate the normalized channels, then apply the color to the four vertices of a quad:
The decoder is deliberately kept as a separate call in this measurement. On Windows x64, main promotes the four fields and emits scalar stores for each vertex. This PR preserves vector copies. An excerpt from the FullOpts fast path (unchanged setup and bounds checks omitted):
The bounds-check fallback has the same copy transformation. Scalar-load placement and register assignment also change, and some branches become short encodings. Each vector copy still reloads the source from the stack.
| Windows x64 compilation | Before bytes | After bytes | Reduction | Instructions | Static PerfScore (lower is better) |
| --- | ---: | ---: | ---: | ---: | ---: |
| FullOpts | 394 | 271 | 31.2% | 94 -> 70 | 41.33 -> 33.46 |
| Tier1 with PGO | 329 | 269 | 18.2% | 86 -> 70 | 23.33 -> 22.38 |
Counts cover the whole method, including fallback paths. Tier1 results use the same valid/invalid input sequence on both JITs. Linux x64 FullOpts is unchanged at 573 bytes. Behavioral checks pass on both JITs on Windows and Linux; inputs include a valid color and negative, above-one, NaN and infinite channel values. This is a focused example outside the SuperPMI corpus. Execution speed has not been measured.
Cost accounting
Reuse existing access counters and register-move costs, amortizing the original vector move across fields. The added charge applies when there are more distinct primitive accesses than native-sized copy parts and the participating accesses are smaller than a native word. Count scalar reads and induced accesses without counting the same access twice. Fields with only definitions and no induced accesses remain excluded: promotion can propagate their values into whole-struct copies and eliminate the local. Struct initializations, including nonzero
GT_INIT_VALvalues, are identified withIsInitVal()and excluded from the existing copy-destination counts; no extra per-access counters are added. Call-result readbacks retain their separate cost.Whole-local copies whose matching fields are already promoted at the other endpoint receive credit through the existing induced-access pass, for both regular and physical promotion. Partial copies retain their charge, including when the destination has only induced fields. The induced-sibling scan runs only for accesses eligible for this heuristic and when the existing full-copy count is nonzero. This remains a per-field estimate based on access entries, rather than joint evaluation of the final replacement set.
The focused
CopyInducedToArraytest copies a 16-byte subrange at offset 8 from a 24-byte promoted source, then stores the destination into eight array elements. The destination has no explicit field reads. Main emits scalar array stores; this PR preserves vector copies, reducing the Windows x64 method from 472 to 373 bytes. This example is outside the retained corpus.SuperPMI
Eleven Windows x64 collections, Checked JITs, identical settings and loop alignment disabled:
| Comparable contexts | Before code bytes | After code bytes | Assembly differences | Regressions |
| ---: | ---: | ---: | ---: | ---: |
| 983,234 | 238,457,491 | 238,457,491 | 0 | 0 |
Zero compilation failures. The recordings contain 983,424 contexts; 190 lack recorded answers needed by both JITs and are excluded. The existing corpus demonstrates no observed generated-code regressions, but does not contain a measured improvement from this PR.
Compilation throughput
Release-JIT PIN instruction counts, main to this PR:
| Collection | Comparable contexts | Before JIT instructions | After JIT instructions | Change |
| --- | ---: | ---: | ---: | ---: |
| benchmarks.run | 51,617 | 87,398,875,380 | 87,425,813,848 | +0.03082% |
| HashSet PGO | 1,307 | 558,877,354 | 558,947,764 | +0.01260% |
Both sides have nonzero counts in every comparable context; HashSet has six shared missing recordings. These counts measure compilation work for the complete PR, not generated-code execution speed. The small measured increases do not justify adding cached per-local state.
Validation
Windows x64 Checked and Linux x64 Release JIT builds, plus Checked ARM64 and x86 cross-JIT builds.
SharedVectorCopyCost: Windows normal, forced physical promotion, JitStress=2 and tiering/PGO runs; Linux behavioral and applicable x64 codegen checks.Codegen checks cover vector array copies, constant-Guid copies without a stack temporary, and shared copies with register and stack arguments. A four-field case exercises a regularly promoted endpoint, with bare
X64codegen checks on Windows and Linux. Disabling only the regular-endpoint credit makes the check fail on Windows; Linux already chooses that code shape without the credit. The JIT dump confirms regular destination promotion and induced physical source promotion on Windows.Test-wrapper MSBuild evaluation confirms exactly one registration on Windows, Android, iOS, Mono browser and CoreCLR browser: conditional child project on supported targets, inline source otherwise. Mobile execution was not run.
Induced-only partial-copy and mixed-width tests use bare
X64checks and pass on Windows and Linux. The mixed-width case combines alongwith fourshortfields and verifies the no-charge behavior. Removing only the native-width sibling guard causes its Windows codegen check to fail on a stack reload.MSBuild evaluation confirms process isolation and explicit
DOTNET_JitEnablePhysicalPromotion=1, alongside the existing optimized-code settings.Nonzero
initblkcoverage: the dump confirmsGT_INIT_VALreaches physical promotion and is excluded from the full-struct copy-destination count. Positive/negative inputs and codegen checks cover constant propagation after a partial overwrite.Windows-target ReadyToRun codegen validation with matching-source crossgen2 and Checked cross-JITs: ARM64
CopyToArrayis 292 -> 248 bytes, andCopyInducedToArrayis 448 -> 380 bytes. ARM64 checks require both paired vector stores for the four direct copies and all four paired vector stores for the eight induced copies; both methods reject main at the vector-store checks. On x86 these methods remain 242 and 409 bytes: theirintfields are native-sized and intentionally uncharged. New x86 checks cover that exclusion; initialization checks cover both targets. ARM64 and x64 have eight-byte pointers; x86 has four.Array-copy checks count all eight induced stores on Windows/Linux x64 and all four direct stores on Windows x64. Checks end at the first return so fallback-path stores cannot satisfy missing fast-path copies, and do not require repeated source loads. SuperFileCheck passes against retained PR listings on Windows/Linux x64 and ARM64; removing each fast-path vector store individually causes all 26 mutation cases to fail. x86 checks also pass.
Changed-line clang-format,
git diff --check, and added-source ASCII checks.The full JIT test tree has not been rerun. ARM64 and x86 validation compiles and checks generated code; no execution on those targets is claimed. Code size, static PerfScore and JIT instruction counts do not establish generated-code execution speed.