Skip to content

Conversation

@333fred
Copy link
Member

@333fred 333fred commented Aug 29, 2025

Implements hoisting of ref locals in runtime async. Closes #79763. Commit 1 is a refactoring to extract the direct hoisting logic into a helper type that can be used by runtime async as well, and it has no semantic impact beyond that. Commit 2 actually implements the changes in runtime async, so I recommend reviewing this commit-by-commit for ease of reading. @jcouv @RikkiGibson @dotnet/roslyn-compiler for review.

Relates to test plan #75960

@333fred 333fred marked this pull request as ready for review August 29, 2025 23:16
@333fred 333fred requested a review from a team as a code owner August 29, 2025 23:16
@333fred 333fred requested review from RikkiGibson and jcouv August 29, 2025 23:16
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jakobbotsch @agocke, I'd appreciate if you could look through the expected IL changes here and make sure that they match your expectations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spot checked a few of them and they looked good (and valid). I can do some more validation once we start fuzzing with this new change merged into Roslyn.

Comment on lines +1288 to +1298
IL_0013: ldloc.0
IL_0014: ldc.i4.3
IL_0015: ldelem.i4
IL_0016: pop
IL_0017: ldloc.0
IL_0018: ldc.i4.3
IL_0019: ldelem.i4
IL_001a: stloc.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to load array[3] twice, discarding the first value. Unexpected?

Copy link
Member

@jcouv jcouv Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect we will load array[3] once before the await (to read the old value) and once after (to assigned the updated value). If we're dropping the first value, then the result should be incorrect. @333fred Could we add verification of the resulting values (in this test or separate)?
Similarly, if we do M()[3] += await ..., I expect we'll have a side-effect spilling the result of M() once, but we should still do the indexing with [3] twice. Do we have test showing side-effects and order of evaluation directly? (this IL is large enough that it's not trivial to tell...)

Copy link
Member

@jakobbotsch jakobbotsch Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IL here loads the element twice and then also stores the element once. So there's three indexing operations happening: two loads done by the IL right above my comment, before awaiting, and one store done by the IL below, after awaiting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. FWIW, it looks like the state machine IL above also had this problem (IL_003d: pop)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. FWIW, it looks like the state machine IL above also had this problem (IL_003d: pop)

Thanks for looking at that Julien. Given that, I plan to leave this as is for now, unless it is fairly obvious from a quick investigation.

Copy link
Member

@jcouv jcouv Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the extra/discarded read is the sacrificial evaluation that we're discussing elsewhere.
Is the following correct:
When doing an assignment arr[index] = await M();, we cache parts with side-effects, and do a sacrificial evaluation of the hoisted/stripped indexing operation before the await. The index-out-of-bounds exception is thrown before M() is evaluated.

Then I think what we're seeing here is that in a compound assignment arr[index] += await M(); we're doing a sacrificial evaluation and an indexing/read before the await, then an indexing/write after the await.

If that understanding is correct, consider filing an issue to optimize the IL (remove sacrificial evaluation) in compound scenarios.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #80147.

@jcouv jcouv self-assigned this Sep 2, 2025
@jcouv
Copy link
Member

jcouv commented Sep 2, 2025

public class CodeGenAsyncSpillTests : EmitMetadataTestBase

nit: consider adding [CompilerTrait(CompilerFeature.Async)] to CodeGenAsyncSpillTests to make it easier to run all the async-related tests #Closed


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs:17 in 8c4a58b. [](commit_id = 8c4a58b, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Sep 2, 2025

        wrapper = new BaseMethodWrapperSymbol(containingType, methodBeingWrapped, syntax, methodName);

Not related to this PR: I don't expect we'll need this kind of base method wrapper for runtime-async scenario, but it would be good to confirm and check that we have coverage. For example, BaseAccessInClosure_14_WithILCheck and possibly other tests in that file would be good to cover with runtime-async enabled. #Pending


Refers to: src/Compilers/CSharp/Portable/Lowering/MethodToClassRewriter.cs:168 in 8c4a58b. [](commit_id = 8c4a58b, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Sep 2, 2025

    public void SpillCollectionInitializer()

Regarding SpillCollectionInitializer and possibly some other tests: is the plan to enable runtime-async compilation in every test in this file? #Closed


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs:7295 in 8c4a58b. [](commit_id = 8c4a58b, deletion_comment = False)

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with review pass (commit 2). I'll look at the tests tomorrow

@jcouv
Copy link
Member

jcouv commented Sep 3, 2025

                [F]: Unexpected type on the stack. { Offset = 0x40, Found = Int32, Expected = ref '[System.Runtime]System.Threading.Tasks.Task`1<int32>' }

Not blocking this PR: I failed to notice this in previous PRs, why is the expected type on stack ref Task<int> rather than Task<int>?


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs:1476 in 8c4a58b. [](commit_id = 8c4a58b, deletion_comment = False)

@333fred
Copy link
Member Author

333fred commented Sep 4, 2025

        var verifier = CompileAndVerify(comp, verify: Verification.Fails with

I updated the tests that have baselines with verification already, but I am not going to update the ones that do not have existing baseline running today in this pr at this point (and this particular test is one such example).


In reply to: 3253071615


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs:1259 in 83e3611. [](commit_id = 83e3611, deletion_comment = False)

@333fred
Copy link
Member Author

333fred commented Sep 4, 2025

        wrapper = new BaseMethodWrapperSymbol(containingType, methodBeingWrapped, syntax, methodName);

I'll add verification to CodeGenAsyncTests.Property21.


In reply to: 3252991712


Refers to: src/Compilers/CSharp/Portable/Lowering/MethodToClassRewriter.cs:168 in 8c4a58b. [](commit_id = 8c4a58b, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Sep 5, 2025

        // Sacrificial read should ensure that `await` never happens

nit: I'd put that inside Assign (same comment below)


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs:11915 in 83e3611. [](commit_id = 83e3611, deletion_comment = False)

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (commit 4) modulo PEVerify errors hit in CI

@333fred
Copy link
Member Author

333fred commented Sep 8, 2025

@RikkiGibson @dotnet/roslyn for a second review

@RikkiGibson
Copy link
Member

Will review tomorrow

@RikkiGibson RikkiGibson self-assigned this Sep 9, 2025
Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, had various comments and questions though.

IL_001e: call "void System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter<System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter>(System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter)"
IL_0023: ldloca.s V_0
IL_0025: call "void System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.GetResult()"
IL_002a: ldfld "int S.i"
Copy link
Member

@RikkiGibson RikkiGibson Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh, so this is operating on the "S" pushed to stack by IL_0001: ldobj? i.e. we can actually hoist 'this' by-value to the stack, an explicit local is not needed? Neat.

Maybe a similar thing as the other test which eliminated the temp for 'this'.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (commit 7)

@333fred 333fred enabled auto-merge (rebase) September 11, 2025 16:40
@333fred 333fred disabled auto-merge September 11, 2025 18:27
Copy link
Member

@jjonescz jjonescz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last commit LGTM

@333fred 333fred enabled auto-merge (rebase) September 11, 2025 20:08
@333fred
Copy link
Member Author

333fred commented Sep 11, 2025

Rebased to make the merge clean; I want to keep these two as separate commits in history for future reference, so squash wouldn't have worked.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (commit 2)

@333fred 333fred merged commit 794a536 into dotnet:main Sep 11, 2025
23 of 24 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Sep 11, 2025
@333fred 333fred deleted the ref-hoisting branch September 11, 2025 21:17
@akhera99 akhera99 modified the milestones: Next, 18.0 P1, 18.0 P2 Sep 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async support for hoisting by-refs

6 participants