@@ -31,11 +31,16 @@ protected override void EmitWorkloadCore()
3131 typeof ( ValueTaskAwaiter < bool > ) ,
3232 FieldAttributes . Private
3333 ) ;
34- var benchmarkAwaiterField = asyncStateMachineTypeBuilder . DefineField (
35- "<>u__2" ,
36- awaitableInfo . AwaiterType ,
37- FieldAttributes . Private
38- ) ;
34+ // Roslyn dedupes hoisted awaiter fields by type — when the workload's awaiter matches the
35+ // workloadValueTaskSource awaiter (e.g., the workload itself returns ValueTask<bool>) only
36+ // `<>u__1` is emitted, and both awaits reuse it.
37+ var benchmarkAwaiterField = awaitableInfo . AwaiterType == workloadContinuerAwaiterField . FieldType
38+ ? workloadContinuerAwaiterField
39+ : asyncStateMachineTypeBuilder . DefineField (
40+ "<>u__2" ,
41+ awaitableInfo . AwaiterType ,
42+ FieldAttributes . Private
43+ ) ;
3944 EmitMoveNextImpl ( ) ;
4045 var asyncStateMachineType = CompleteAsyncStateMachineType ( asyncMethodBuilderType , builderInfo ) ;
4146
@@ -48,19 +53,27 @@ void EmitMoveNextImpl()
4853 var resultType = awaitableInfo . ResultType ;
4954 var isCompleteAwaiterLocal = ilBuilder . DeclareLocal ( typeof ( ValueTaskAwaiter < bool > ) ) ;
5055 var isCompleteAwaitableLocal = ilBuilder . DeclareLocal ( typeof ( ValueTask < bool > ) ) ;
51- // The value-type awaitable spill local (Roslyn declares one for ValueTask<T> et al. so it
52- // can take its address for GetAwaiter — reference-type awaitables stay on the stack).
53- var benchmarkAwaitableLocal = Descriptor . WorkloadMethod . ReturnType . IsValueType
54- ? ilBuilder . DeclareLocal ( Descriptor . WorkloadMethod . ReturnType )
55- : null ;
56- // Source local for `T result = await awaitable;` — declared only when the awaiter's
56+ // Source local for `T result = await workloadCall;` — declared only when the awaiter's
5757 // GetResult returns non-void, in which case the template captures the value and pipes
5858 // it through DeadCodeEliminationHelper so the JIT can't elide the producer's work.
59- // Roslyn places this AFTER the (optional) awaitable spill and BEFORE the awaiter temp .
59+ // Roslyn places this BEFORE the awaiter temp and the (optional) awaitable spill .
6060 var resultLocal = resultType == typeof ( void )
6161 ? null
6262 : ilBuilder . DeclareLocal ( resultType ) ;
63- var benchmarkAwaiterLocal = ilBuilder . DeclareLocal ( benchmarkAwaiterField . FieldType ) ;
63+ // Roslyn dedupes locals by type — when the workload awaiter matches the isComplete
64+ // awaiter (e.g., the workload itself returns ValueTask<bool>), it reuses the existing
65+ // local rather than declaring a new one.
66+ var benchmarkAwaiterLocal = benchmarkAwaiterField . FieldType == isCompleteAwaiterLocal . LocalType
67+ ? isCompleteAwaiterLocal
68+ : ilBuilder . DeclareLocal ( benchmarkAwaiterField . FieldType ) ;
69+ // The value-type awaitable spill local (Roslyn declares one for ValueTask<T> et al. so it
70+ // can take its address for GetAwaiter — reference-type awaitables stay on the stack).
71+ // Roslyn places this AFTER the awaiter temp, and dedupes by type just like the awaiter.
72+ var benchmarkAwaitableLocal = Descriptor . WorkloadMethod . ReturnType . IsValueType
73+ ? ( Descriptor . WorkloadMethod . ReturnType == isCompleteAwaitableLocal . LocalType
74+ ? isCompleteAwaitableLocal
75+ : ilBuilder . DeclareLocal ( Descriptor . WorkloadMethod . ReturnType ) )
76+ : null ;
6477 var invokeCountLocal = ilBuilder . DeclareLocal ( typeof ( long ) ) ;
6578 var exceptionLocal = ilBuilder . DeclareLocal ( typeof ( Exception ) ) ;
6679
@@ -92,7 +105,7 @@ void EmitMoveNextImpl()
92105 // var awaitable = workloadValueTaskSource.GetIsComplete();
93106 ilBuilder . EmitLdloc ( thisLocal ! ) ;
94107 ilBuilder . Emit ( OpCodes . Ldflda , fieldsContainerField ) ;
95- ilBuilder . Emit ( OpCodes . Ldfld , workloadContinuerAndValueTaskSourceField ) ;
108+ ilBuilder . Emit ( OpCodes . Ldfld , workloadValueTaskSourceField ) ;
96109 ilBuilder . Emit ( OpCodes . Callvirt , typeof ( WorkloadValueTaskSource ) . GetMethod ( nameof ( WorkloadValueTaskSource . GetIsComplete ) , BindingFlags . Public | BindingFlags . Instance ) ! ) ;
97110 ilBuilder . EmitStloc ( isCompleteAwaitableLocal ) ;
98111 // var awaiter = awaitable.GetAwaiter();
@@ -260,7 +273,7 @@ void EmitMoveNextImpl()
260273 // var awaitable = workloadValueTaskSource.SetResultAndGetIsComplete(startedClock.GetElapsed());
261274 ilBuilder . EmitLdloc ( thisLocal ! ) ;
262275 ilBuilder . Emit ( OpCodes . Ldflda , fieldsContainerField ) ;
263- ilBuilder . Emit ( OpCodes . Ldfld , workloadContinuerAndValueTaskSourceField ) ;
276+ ilBuilder . Emit ( OpCodes . Ldfld , workloadValueTaskSourceField ) ;
264277 ilBuilder . Emit ( OpCodes . Ldarg_0 ) ;
265278 ilBuilder . Emit ( OpCodes . Ldflda , startedClockField ) ;
266279 ilBuilder . Emit ( OpCodes . Call , typeof ( StartedClock ) . GetMethod ( nameof ( StartedClock . GetElapsed ) , BindingFlags . Public | BindingFlags . Instance ) ! ) ;
@@ -333,7 +346,7 @@ void EmitMoveNextImpl()
333346 // workloadValueTaskSource.SetException(exception);
334347 ilBuilder . EmitLdloc ( thisLocal ! ) ;
335348 ilBuilder . Emit ( OpCodes . Ldflda , fieldsContainerField ) ;
336- ilBuilder . Emit ( OpCodes . Ldfld , workloadContinuerAndValueTaskSourceField ) ;
349+ ilBuilder . Emit ( OpCodes . Ldfld , workloadValueTaskSourceField ) ;
337350 ilBuilder . EmitLdloc ( exceptionLocal ) ;
338351 ilBuilder . Emit ( OpCodes . Callvirt , typeof ( WorkloadValueTaskSource ) . GetMethod ( nameof ( WorkloadValueTaskSource . SetException ) , [ typeof ( Exception ) ] ) ! ) ;
339352 // result = default;
0 commit comments