You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Widespread perf regressions due to RPO layout #102763
@EgorBo Thanks for opening this. I haven't spent too much time looking at the regressions yet, but based on the larger ones I've looked at, I think we need a more general/powerful implementation of #102461. Take GuardedDevirtualization.TwoClassVirtual.Call for instance, which regressed by over 60% on Windows x64 (GuardedDevirtualization.ThreeClassVirtual.Call regressed for similar reasons). Before reordering, the blocks look like this:
The interesting ones are BB03-BB06, which comprise a loop. The hot path is BB03->BB04->BB06->BB03; moving BB05 out-of-line to make the hot path branchless (except for the backward jump) would be ideal. The old layout does this:
I think modifying fgMoveBackwardJumpsToSuccessors to consider forward jumps would be a good start to chipping away at these regressions. However, moving BB06 up to BB04 would break up the former's fallthrough into BB07. That's ok, since that's part of the slow path, but we probably want to update the cost check in fgMoveBackwardJumpsToSuccessors to also check if moving the target block will break up fallthrough into its next block, and whether the edge weights deem it's worth it.
Thoughts on this? cc @AndyAyersMS for visibility. Thanks!
I would suggest we list all the regressions in rank order and then investigate at least the top 20 or so. We also should see what the arm64 data looks like.
Based on the above and the the other fixup actions, it feels like we are getting to the point where we may actually want to create the explicit cost/benefit model to assess incremental improvements. We have a layout configuration A, and some proposed alternate B. Which is better?
If you want to try to build this out, we should talk. The general model is something like the following:
identify 3 cut points in the linear chain of blocks (creating 4 segments overall). First segment must contain at least the entry block; last one can be empty.
swap the middle two segments; if that is lower cost, make that the new configuration
repeat until you can't find any more improvements or give up (drive to local minima)
(or just limit yourself to one pass or something to keep the TP under control)
(the someday refinement: occasionally allow reorderings that drive the cost up, to try and find a global minimum)
Since there are just 3 cut points, if you have an initial cost model it's not too hard to compute the delta to the new cost model, you (mostly) just need to recost the segment-ending blocks (if the cost model is block-size sensitive then perhaps a bit more, but I don't think we have good size estimates yet?)
For the time being the "identify" step can be finding a single misplaced block and a new proposed location, and then seeing if the cost improves.
I would suggest we list all the regressions in rank order and then investigate at least the top 20 or so.
Sorry about the delay in getting back to this; the script we typically use to collate regression data doesn't seem to work since the perflab test report URLs changed, so I hacked up my own script. Here are the top 20 regressions on Windows x64:
I'll start digging into the overlapping benchmarks (this explains GuardedDevirtualization.TwoClassVirtual.Call and GuardedDevirtualization.ThreeClassVirtual.Call already, and I suspect the theme of hot paths being polluted by a relatively cold (but not "cold" cold) jump will continue to come up).
I'm seeing the same theme of interleaving hot and colder blocks throughout these regressions. The RPO-based layout on its own doesn't seem to leverage edge weights heavily enough, and while fgMoveBackwardJumpsToSuccessors does consider edge weights, its backward jump limitation is blocking lots of potentially profitable moves. System.Memory.Span<Int32>.Reverse illustrates this succinctly. Here's what the new layout does:
Notice how BB06 can be reached from BB04 and BB05, and the former is hotter, but doesn't fall into BB06. If fgMoveBackwardJumpsToSuccessors considered forward jumps, it would move BB06 up. For a more involved example, let's look at System.Text.Perf_Ascii.Equals_Bytes. Here's the new layout:
If we considered moving forward jumps, we'd have the following decision tree:
BB03->BB04 is hotter than BB02->BB04, so move BB04 up.
BB04->BB06 is hot, and BB04 is the only pred of BB06, but BB02 is now between them, so move BB06 up; we're basically bubbling BB02 down the block list. Now we have this:
And for System.String.TrimWhiteSpaceHelper from System.Tests.Perf_String.TrimEnd (in particular, we could consider creating fallthrough for BB29->BB23->BB27->BB28):
And for System.Tests.Perf_Random.NextBytes_unseeded (an interesting case in that the exceptional path is the hot path based on edge weights, even though the throw block has a weight of 0 due to RyuJIT's flowgraph semantics -- I guess if our post-RPO changes suggest a "rarely-run" block should be moved up because it isn't that rare, maybe we should unmark it as not rare?):
Based on the above and the the other fixup actions, it feels like we are getting to the point where we may actually want to create the explicit cost/benefit model to assess incremental improvements.
I'd be interested in pursuing this, though based on the above, I think it might be worthwhile trying to implement a more general post-RPO pass that does the moves I described above, since it seems simple enough.
All the other regressed GuardedDevirtualization benchmarks (TwoClassVirtual, ThreeClassVirtual, ThreeClassInterface) have the same shape that can now fall out of the hot loop into the return block. Those feature heavily in the top regressions on x64 and arm64, and the new layout looks optimal, so I'd be surprised if these regressions aren't resolved.
The decision to move BB05 ends up breaking fallthrough from BB01 into BB06 without introducing any new fallthrough. We could fix this in fgMoveHotBlocks by not moving backward jumps if the previous block falls into the block with the jump, and breaking this fallthrough won't introduce new fallthrough into the next block. But this starts to get a bit pattern-matchy, so perhaps we accept this for now until we have a more general (i.e. 3-opt) solution.
Sorry for all the comment spam. I've looked at the remaining top regressions, and I haven't seen anything noteworthy -- in general, #102927 seems to do a good job of maximizing fallthrough on hot paths, so long as the profile data is sensible. As such, I'm planning on getting that merged for now, and coming back to 3-opt after Preview 6.
I'm punting this to .NET 10, as we're planning to continue to iterate on the new layout. We'll re-evaluate any remaining regressions here with each change.
There's been far too much churn since these reports came out for us to be able to pinpoint root causes for changes since then, but I thought it would be interesting to look at how the benchmarks have changed as a proxy for .NET 9 vs 10 layout perf. Here are the arm64 regressions:
The Windows arm64 regressions are too noisy to be enlightening. The Ubuntu arm64 regressions are clearly from #113491, and are tracked in #113912. The GDV benchmarks at the bottom show nice improvements.
I don't see anything in the commit range for the regression in System.Collections.ContainsTrue.Array that points to layout. Everything else looks pretty stable.
The System.IO.Tests.Perf_StreamWriter regressions are probably from #113715. For the remaining ones, some never recovered from before RPO layout, while others have spiked up and down as we churned the layout through .NET 10. In spite of the JCC errata instances, most of these benchmarks seem to have improved.
Since many of the regressed benchmarks have returned or improved upon their baseline on non-Intel platforms, and remaining regressions are tracked, I'm going to close this.
Activity
dotnet-policy-service commentedon May 28, 2024
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
amanasifkhalid commentedon May 28, 2024
@EgorBo Thanks for opening this. I haven't spent too much time looking at the regressions yet, but based on the larger ones I've looked at, I think we need a more general/powerful implementation of #102461. Take
GuardedDevirtualization.TwoClassVirtual.Callfor instance, which regressed by over 60% on Windows x64 (GuardedDevirtualization.ThreeClassVirtual.Callregressed for similar reasons). Before reordering, the blocks look like this:The interesting ones are
BB03-BB06, which comprise a loop. The hot path isBB03->BB04->BB06->BB03; movingBB05out-of-line to make the hot path branchless (except for the backward jump) would be ideal. The old layout does this:In the new layout,
fgMoveBackwardJumpsToSuccessorsonly considers moving backward unconditional jumps, soBB05remains in the way of the hot path:I think modifying
fgMoveBackwardJumpsToSuccessorsto consider forward jumps would be a good start to chipping away at these regressions. However, movingBB06up toBB04would break up the former's fallthrough intoBB07. That's ok, since that's part of the slow path, but we probably want to update the cost check infgMoveBackwardJumpsToSuccessorsto also check if moving the target block will break up fallthrough into its next block, and whether the edge weights deem it's worth it.Thoughts on this? cc @AndyAyersMS for visibility. Thanks!
AndyAyersMS commentedon May 28, 2024
I would suggest we list all the regressions in rank order and then investigate at least the top 20 or so. We also should see what the arm64 data looks like.
Based on the above and the the other fixup actions, it feels like we are getting to the point where we may actually want to create the explicit cost/benefit model to assess incremental improvements. We have a layout configuration A, and some proposed alternate B. Which is better?
If you want to try to build this out, we should talk. The general model is something like the following:
Since there are just 3 cut points, if you have an initial cost model it's not too hard to compute the delta to the new cost model, you (mostly) just need to recost the segment-ending blocks (if the cost model is block-size sensitive then perhaps a bit more, but I don't think we have good size estimates yet?)
For the time being the "identify" step can be finding a single misplaced block and a new proposed location, and then seeing if the cost improves.
amanasifkhalid commentedon May 29, 2024
Sorry about the delay in getting back to this; the script we typically use to collate regression data doesn't seem to work since the perflab test report URLs changed, so I hacked up my own script. Here are the top 20 regressions on Windows x64:
And on Linux x64:
I'll start digging into the overlapping benchmarks (this explains
GuardedDevirtualization.TwoClassVirtual.CallandGuardedDevirtualization.ThreeClassVirtual.Callalready, and I suspect the theme of hot paths being polluted by a relatively cold (but not "cold" cold) jump will continue to come up).amanasifkhalid commentedon May 29, 2024
I'm seeing the same theme of interleaving hot and colder blocks throughout these regressions. The RPO-based layout on its own doesn't seem to leverage edge weights heavily enough, and while
fgMoveBackwardJumpsToSuccessorsdoes consider edge weights, its backward jump limitation is blocking lots of potentially profitable moves.System.Memory.Span<Int32>.Reverseillustrates this succinctly. Here's what the new layout does:Notice how
BB06can be reached fromBB04andBB05, and the former is hotter, but doesn't fall intoBB06. IffgMoveBackwardJumpsToSuccessorsconsidered forward jumps, it would moveBB06up. For a more involved example, let's look atSystem.Text.Perf_Ascii.Equals_Bytes. Here's the new layout:If we considered moving forward jumps, we'd have the following decision tree:
BB03->BB04is hotter thanBB02->BB04, so moveBB04up.BB04->BB06is hot, andBB04is the only pred ofBB06, butBB02is now between them, so moveBB06up; we're basically bubblingBB02down the block list. Now we have this:BB06->BB07is hotter thanBB05->BB07, so moveBB07up. We've bubbledBB02andBB05down.BB07->BB08is hot, andBB07is the only pred ofBB08, so moveBB08up.BB08->BB10is hot, andBB10's other pred doesn't fall into it anyway, so moveBB10` up. Here's what we have:BB10has no successors.BB02is a backward jump, butBB03->BB04is hotter, so don't move anything.BB05.BB09is rarely-run, so we don't care about it.For a few other short examples, just to build my case, here's the new layout of
System.Text.Encodings.Web.Tests.Perf_Encoders.EncodeUtf8:And for
System.String.TrimWhiteSpaceHelperfromSystem.Tests.Perf_String.TrimEnd(in particular, we could consider creating fallthrough forBB29->BB23->BB27->BB28):And for
System.Tests.Perf_Random.NextBytes_unseeded(an interesting case in that the exceptional path is the hot path based on edge weights, even though the throw block has a weight of 0 due to RyuJIT's flowgraph semantics -- I guess if our post-RPO changes suggest a "rarely-run" block should be moved up because it isn't that rare, maybe we should unmark it as not rare?):I'd be interested in pursuing this, though based on the above, I think it might be worthwhile trying to implement a more general post-RPO pass that does the moves I described above, since it seems simple enough.
AndyAyersMS commentedon May 29, 2024
I am surprised some of these are so large. Do we have any results for AMD HW? All the reports above are for Intel.
AndyAyersMS commentedon May 29, 2024
ADX data for the three class virtual test. Strongly suggests that this may be JCC errata kicking in.
Of course this is a two-way street, some of the improvements may be like this too.
AndyAyersMS commentedon May 29, 2024
Randomly looking at improvements, it seems most are also intel only. But not all. Here's
Benchstone.BenchI.BubbleSort2.Test@DrewScoggins is looking into whether there are unfiled AMD64 reports out there. From what I can tell the newer AMD64 hardware is mostly indifferent.
18 remaining items
amanasifkhalid commentedon Jun 14, 2024
GuardedDevirtualization.TwoClassInterface:Callhas a similar layout shape, and the updatedfgMoveHotBlocksseems to handle it better:All the other regressed
GuardedDevirtualizationbenchmarks (TwoClassVirtual,ThreeClassVirtual,ThreeClassInterface) have the same shape that can now fall out of the hot loop into the return block. Those feature heavily in the top regressions on x64 and arm64, and the new layout looks optimal, so I'd be surprised if these regressions aren't resolved.JIT: Consider block weights instead of BBF_RUN_RARELY flag in fgMoveC…
amanasifkhalid commentedon Jun 15, 2024
LinqBenchmarks:Count00ForXhighlights a case where our loop misrotation fix makes the wrong decision. Here's the initial RPO layout:And the final layout:
The decision to move
BB05ends up breaking fallthrough fromBB01intoBB06without introducing any new fallthrough. We could fix this infgMoveHotBlocksby not moving backward jumps if the previous block falls into the block with the jump, and breaking this fallthrough won't introduce new fallthrough into the next block. But this starts to get a bit pattern-matchy, so perhaps we accept this for now until we have a more general (i.e. 3-opt) solution.amanasifkhalid commentedon Jun 15, 2024
Sorry for all the comment spam. I've looked at the remaining top regressions, and I haven't seen anything noteworthy -- in general, #102927 seems to do a good job of maximizing fallthrough on hot paths, so long as the profile data is sensible. As such, I'm planning on getting that merged for now, and coming back to 3-opt after Preview 6.
JIT: Move targets of hot jumps to create fallthrough post-RPO layout (#…
amanasifkhalid commentedon Jun 17, 2024
I've merged #102927 -- fingers crossed we see an improvement in the next triage...
amanasifkhalid commentedon Jul 26, 2024
I'm punting this to .NET 10, as we're planning to continue to iterate on the new layout. We'll re-evaluate any remaining regressions here with each change.
amanasifkhalid commentedon Apr 11, 2025
There's been far too much churn since these reports came out for us to be able to pinpoint root causes for changes since then, but I thought it would be interesting to look at how the benchmarks have changed as a proxy for .NET 9 vs 10 layout perf. Here are the arm64 regressions:
1.14
1.33
1.56
1.16
2.22
1.90
1.33
1.61
1.07
1.47
1.05
1.40
1.11
1.11
1.13
1.08
2.05
1.53
1.16
1.06
1.06
1.06
1.07
1.82
1.07
1.06
1.06
1.45
2.80
1.06
1.06
1.06
1.99
1.08
1.18
3.00
1.31
1.18
1.05
1.23
1.48
1.09
1.12
1.96
2.07
2.20
2.28
2.26
1.96
1.64
2.03
1.33
1.91
2.20
1.85
1.48
1.48
The Windows arm64 regressions are too noisy to be enlightening. The Ubuntu arm64 regressions are clearly from #113491, and are tracked in #113912. The GDV benchmarks at the bottom show nice improvements.
amanasifkhalid commentedon Apr 12, 2025
And here are the improved arm64 benchmarks compared to main:
0.89
0.92
0.93
0.93
0.92
0.92
0.89
0.84
0.82
0.87
0.86
0.83
0.83
0.84
0.82
0.83
0.83
0.83
0.90
0.84
0.82
0.76
0.87
0.75
0.83
0.82
0.75
0.90
0.88
0.92
0.65
0.79
0.84
0.73
I don't see anything in the commit range for the regression in
System.Collections.ContainsTrue.Arraythat points to layout. Everything else looks pretty stable.amanasifkhalid commentedon Apr 12, 2025
I'm not putting a lot of weight into them due to all the JCC errata instances, but here are the Intel regressions:
1.19
1.45
1.13
1.16
1.18
1.16
1.07
1.16
1.24
1.10
1.12
1.12
1.08
1.10
1.24
1.12
1.11
1.19
1.09
1.12
1.05
1.18
1.18
1.08
1.18
1.13
2.45
1.12
1.08
1.22
1.16
1.14
1.08
1.07
1.38
3.20
1.10
2.64
1.11
1.07
1.67
1.30
1.13
1.11
1.09
1.17
1.13
1.29
1.13
1.15
1.18
1.09
1.08
1.09
1.35
1.11
1.16
1.34
1.29
1.13
1.29
1.13
1.21
1.11
1.07
1.15
1.08
1.22
1.39
1.72
1.76
1.92
1.24
1.12
1.13
1.28
The
System.IO.Tests.Perf_StreamWriterregressions are probably from #113715. For the remaining ones, some never recovered from before RPO layout, while others have spiked up and down as we churned the layout through .NET 10. In spite of the JCC errata instances, most of these benchmarks seem to have improved.amanasifkhalid commentedon Apr 12, 2025
Since many of the regressed benchmarks have returned or improved upon their baseline on non-Intel platforms, and remaining regressions are tracked, I'm going to close this.