-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Reduce Execution Context Save+Restore #15629
Conversation
|
@dotnet-bot test Windows_NT x64 Checked corefx_baseline |
843c467
to
698ca00
Compare
698ca00
to
6188b76
Compare
@dotnet-bot test Windows_NT x64 Checked corefx_baseline |
x 1.58 speed up for Pre
Post
https://gist.github.com/benaadams/73fedf79313a90bdef4f4f617cbb7aa8 |
Corresponding corert change dotnet/corert#5153 |
PTAL @stephentoub |
Thread currentThread = Thread.CurrentThread; | ||
ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher); | ||
try | ||
// Async state machines are required not to throw, so no need for try/finally here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
corert specifies this corert/.../Start<TStateMachine>(ref TStateMachine stateMachine)
; valid approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would resolve https://github.com/dotnet/coreclr/issues/11156
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As described in #11156, this is debatable. Let's keep PRs focused on one thing rather than trying to cram such changes together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
Any impact when not using the default? |
@@ -130,126 +108,164 @@ public static bool IsFlowSuppressed() | |||
|
|||
public static void Run(ExecutionContext executionContext, ContextCallback callback, Object state) | |||
{ | |||
// Note: ExecutionContext.Run is an extremly hot function and used by every await, threadpool execution etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "threadpool execution etc" => "ThreadPool execution, etc."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also typo: extremly
|
||
Thread currentThread = Thread.CurrentThread; | ||
ExecutionContextSwitcher ecsw = default(ExecutionContextSwitcher); | ||
// Capture references to Thread Contexts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: move the comment up a line
|
||
Thread currentThread = Thread.CurrentThread; | ||
ExecutionContextSwitcher ecsw = default(ExecutionContextSwitcher); | ||
// Capture references to Thread Contexts | ||
ref ExecutionContext current = ref currentThread.ExecutionContext; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: current => currentExecutionCtx
// Store current ExecutionContext and SynchronizationContext as "previous" | ||
// This allows us to restore them and undo any Context changes made in callback.Invoke | ||
// so that they won't "leak" back into caller. | ||
ExecutionContext previous = current; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: previous => previousExecutionCtx
ref ExecutionContext current = ref currentThread.ExecutionContext; | ||
ref SynchronizationContext currentSyncCtx = ref currentThread.SynchronizationContext; | ||
|
||
// Store current ExecutionContext and SynchronizationContext as "previous" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: missing period at end of sentence.
} | ||
|
||
private static void OnContextChanged(ExecutionContext previous, ExecutionContext current) | ||
internal static void Restore(ref ExecutionContext current, ExecutionContext next) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restore
seems like the wrong naming for this. SetCurrentContext
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, maybe currentRef
instead of current
would make this easier to understand when reading the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to OnValuesChanged
as it only gets triggered if at least once side has notifications
ecsw.Undo(currentThread); | ||
throw; | ||
// context before any of our callers' EH filters run. | ||
edi = ExceptionDispatchInfo.Capture(ex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this going to change the call stack from what it would have been, adding in the whole "rethrown from" gook? That's going to affect pretty much every async invocation that incurs an exception, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its the same in both cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? This:
using System;
using System.Runtime.ExceptionServices;
class Program
{
static void Main()
{
try { Foo1(); } catch (Exception e) { Console.WriteLine(e.StackTrace); }
Console.WriteLine();
try { Foo2(); } catch (Exception e) { Console.WriteLine(e.StackTrace); }
}
static void Foo1()
{
try { throw new Exception("test"); }
catch { throw; }
}
static void Foo2()
{
ExceptionDispatchInfo edi = null;
try { throw new Exception("test"); }
catch (Exception e) { edi = ExceptionDispatchInfo.Capture(e); }
edi.Throw();
}
}
produces this for me:
>corerun test.exe
at Program.Foo1() in c:\Users\stoub\Desktop\CoreClrTest\test.cs:line 16
at Program.Main() in c:\Users\stoub\Desktop\CoreClrTest\test.cs:line 8
at Program.Foo2() in c:\Users\stoub\Desktop\CoreClrTest\test.cs:line 22
--- End of stack trace from previous location where exception was thrown ---
at Program.Main() in c:\Users\stoub\Desktop\CoreClrTest\test.cs:line 10
You're saying though that with your change that --- End of stack trace...
doesn't show up when an exception emerges from EC.Run?
(It's not a deal-breaker, but given how common this method is, it'd be nice to avoid the extra gook. If it's not possible, ok.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Been trying to get this to show up in async without much luck; but have since realized that async never lets an exception get caught here (i.e. its already caught it to fail the Task)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added issue for EDI https://github.com/dotnet/coreclr/issues/15779
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just need to change StackTrace from
if (sf.GetIsLastFrameFromForeignExceptionStackTrace() &&
!isAsync) // Skip EDI boundary for async
to
if (sf.GetIsLastFrameFromForeignExceptionStackTrace() &&
!isAsync && // Skip EDI boundary for async
declaringType != typeof(ExecutionContext)) // Skip EDI boundary for ExecutionContext.Run
However would need to rebase; and also I said I'd finished making changes to this PR 😉
internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw) | ||
{ | ||
Debug.Assert(currentThread == Thread.CurrentThread); | ||
if (current != previous) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any benefit to storing a bool earlier when we do the previous != executionContext
check and using that bool here rather than redoing the comparison with the ref?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess current
could have changed back to previous
already, could be if(executionContextChanged && current != previous)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would be better, however the callee may have changed the EC by setting an AsyncLocal
or unpaired SuppressFlow
/RestoreFlow
call; so it needs to check the Thread's contexts again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so it needs to check the Thread's contexts again?
Yup, you're right.
foreach (IAsyncLocal local in previous.m_localChangeNotifications) | ||
{ | ||
previous.m_localValues.TryGetValue(local, out object previousValue); | ||
object currentValue = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The = null
should not be necessary, in which case it can be out object currentValue
as on the previous line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind, I missed the ?
in the next?
on the next line.
{ | ||
newValues = AsyncLocalValueMap.Empty.Set(local, newValue);; | ||
newChangeNotifications = Array.Empty<IAsyncLocal>(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you measured the cost of setting async locals with this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't entirely happy with this; will see if can improve
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is better now
// Run the MoveNext method within a copy-on-write ExecutionContext scope. | ||
// This allows us to undo any ExecutionContext changes made in MoveNext, | ||
Thread currentThread = Thread.CurrentThread; | ||
// Capture references to Thread Contexts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: same nits as earlier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM aside from other comments, thanks!
@@ -130,126 +108,164 @@ public static bool IsFlowSuppressed() | |||
|
|||
public static void Run(ExecutionContext executionContext, ContextCallback callback, Object state) | |||
{ | |||
// Note: ExecutionContext.Run is an extremly hot function and used by every await, threadpool execution etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also typo: extremly
internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw) | ||
{ | ||
Debug.Assert(currentThread == Thread.CurrentThread); | ||
if (current != previous) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess current
could have changed back to previous
already, could be if(executionContextChanged && current != previous)
6188b76
to
d18b7c9
Compare
Think I have a version that may improve both default and non-default (without notifications); setting up some scenarios to test properly. |
d18b7c9
to
016ed58
Compare
@stephentoub, should we go ahead and merge this? |
I'll take one more look tomorrow. Thanks. |
@dotnet-bot test CentOS7.1 x64 Release Innerloop Build and Test please |
No longer exist on CI |
Oh, you're right. Ok. |
/// <summary>Initiates the builder's execution with the associated state machine.</summary> | ||
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam> | ||
/// <param name="stateMachine">The state machine instance, passed by reference.</param> | ||
[DebuggerStepThrough] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why DebuggerStepThrough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, nevermind, I see that it's there on the original for some reason. Not sure why it's there either, but we can leave this for consistency.
Thread.CurrentThread.ExecutionContext = | ||
new ExecutionContext(newValues, newChangeNotifications, current.m_isFlowSuppressed); | ||
Thread.CurrentThread.ExecutionContext = | ||
(!isFlowSuppressed && newValues.GetType() == typeof(AsyncLocalValueMap.EmptyAsyncLocalValueMap)) ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what situation could newValues be empty here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, if null was set as the value for an already empty map I guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the value in OneElementAsyncLocalValueMap
is set to null
@@ -94,7 +94,8 @@ static internal void IOCompletionCallback_Context(Object state) | |||
overlapped = OverlappedData.GetOverlappedFromNative(pOVERLAP).m_overlapped; | |||
helper = overlapped.iocbHelper; | |||
|
|||
if (helper == null || helper._executionContext == null || helper._executionContext == ExecutionContext.Default) | |||
ExecutionContext context = helper?._executionContext; | |||
if (context == null || context.IsDefault) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If helper is null, don't we end up with an extra null check (checking context for null when we otherwise wouldn't)? Or is the JIT able to remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No doesn't, will change
(IThreadPoolWorkItem)new QueueUserWorkItemCallback(callBack, state, context); | ||
IThreadPoolWorkItem tpcallBack = (context == null || !context.IsDefault) ? | ||
new QueueUserWorkItemCallback(callBack, state, context) : | ||
(IThreadPoolWorkItem)new QueueUserWorkItemCallbackDefaultContext(callBack, state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine, but I'm curious why you swapped the polarity here. Did that have an impact on something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So could combine the context == null || !context.IsDefault
tests; otherwise it was a bit ugly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context != null && context.IsDefault
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have over thought it 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted
@dotnet-bot test Windows_NT x64 Checked corefx_baseline |
coreclr and corefx out of sync
|
I'll fix it. My change hasn't mirrored to corefx yet, and I didn't actually know that these legs did ref validation. |
@dotnet-bot test Windows_NT x64 Checked corefx_baseline |
@benaadams, have you run corefx tests with your change locally? Things are still out-of-sync, e.g.
and if you've verified this locally we can just get it merged. |
Yes and on here on coreclr CI prior to the two feedback tweaks |
Ok. Thanks! |
* Reduce Save+Restore for ExecutionContext * Use flag rather than comparison to static * Skip null check for pre-checked EC.Run * Feedback * Add static helper lookup for default context for TP * Add note for enregistering * Return to Default context when no values * Default + FlowSuppressed Context * Move AsyncMethodBuilder.Start to static non-generic * Feedback Signed-off-by: dotnet-bot <[email protected]>
* Reduce Save+Restore for ExecutionContext * Use flag rather than comparison to static * Skip null check for pre-checked EC.Run * Feedback * Add static helper lookup for default context for TP * Add note for enregistering * Return to Default context when no values * Default + FlowSuppressed Context * Move AsyncMethodBuilder.Start to static non-generic * Feedback Signed-off-by: dotnet-bot-corefx-mirror <[email protected]>
* Reduce Save+Restore for ExecutionContext * Use flag rather than comparison to static * Skip null check for pre-checked EC.Run * Feedback * Add static helper lookup for default context for TP * Add note for enregistering * Return to Default context when no values * Default + FlowSuppressed Context * Move AsyncMethodBuilder.Start to static non-generic * Feedback Signed-off-by: dotnet-bot-corefx-mirror <[email protected]>
* Reduce Save+Restore for ExecutionContext * Use flag rather than comparison to static * Skip null check for pre-checked EC.Run * Feedback * Add static helper lookup for default context for TP * Add note for enregistering * Return to Default context when no values * Default + FlowSuppressed Context * Move AsyncMethodBuilder.Start to static non-generic * Feedback Signed-off-by: dotnet-bot <[email protected]>
* Reduce Save+Restore for ExecutionContext * Use flag rather than comparison to static * Skip null check for pre-checked EC.Run * Feedback * Add static helper lookup for default context for TP * Add note for enregistering * Return to Default context when no values * Default + FlowSuppressed Context * Move AsyncMethodBuilder.Start to static non-generic * Feedback Signed-off-by: dotnet-bot <[email protected]>
x 1.94 speed up for
ExecutionContext.Run(ec, (o) => { }, null);
whereec == Default
x 1.57 speed up for
ExecutionContext.Run(ec, (o) => { }, null);
withAsyncLocal
s (no notifications)Results #15629 (comment)
Alternative approach to #11100 as suggested by @kouvel #11100 (comment)
When
Default
context is used inRun
use it asnull
; which also counts as Default - rather than having two forms ofDefault
context internally (Thread starts will null) and saving and restoring between them.ASP.NET Kestrel Plaintext call counts
Pre
Post
Resolves #11126