-
-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Potential performance improvements #26
Comments
When the This would result in decreased memory consumption for reference types, but I'm not sure if it would help with execution time. On one hand, it would eliminate a virtual call to If this change is made, benchmarks are a must before merging it. [Edit] I investigated making this change, and it ended up costing extra checks to see if the field was a ValueContainer or not (which right now it happily just assumes it is). But I did end up optimizing it so that reference types only create one type |
[Edit] This was done in #55 Another option is combining But this would also cause a I'm currently thinking the downsides outweigh the upside at this time. As an aside, async/await currently can cause a |
For progress improvements:
[Edit] These were done except for the progress retains for [Edit2] |
.Net 5 added the |
This issue which forced us to use a less efficient async method builder in IL2CPP builds was fixed in Unity 2020.3.20f1 and 2021.1.24f1. We can [Edit] Done in #45. |
[Edit] Bad measurement, I was benchmarking the same configuration both times. Here are the real results: Without progress:
With progress:
But the largest difference is still only 11% (48 μs divided by 300 is only 160 ns per |
#54 changed async/await to hook up PromiseRefs directly to prevent StackOverflowExceptions. The implementation uses a pass-through object or function pointer to avoid boxing allocations. public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
SetStateMachine(ref stateMachine);
if (null != default(TAwaiter) && AwaitOverrider<TAwaiter>.IsOverridden())
{
AwaitOverrider<TAwaiter>.AwaitOnCompletedInternal(ref awaiter, _ref);
}
else
{
awaiter.UnsafeOnCompleted(_ref.MoveNext);
}
} That code can be optimized to call the method directly on the awaiter in .Net 5 thanks to the JIT optimizing away the boxes. Unity has not yet implemented the optimization, so the AwaitOverrider implementation will need to remain until they do, but we can still implement the optimization for non-Unity consumption. public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
SetStateMachine(ref stateMachine);
if (null != default(TAwaiter) && awaiter is IPromiseAwaiter)
{
((IPromiseAwaiter) awaiter).AwaitOnCompletedInternal(_ref);
}
else
{
awaiter.UnsafeOnCompleted(_ref.MoveNext);
}
} A quick benchmark showed that change increases the speed by ~10% - 15% in .Net 5. |
Currently in master, benchmark results are showing speedups when dealing with already resolved promises and slowdowns when dealing with pending promises.
[Edit] Updated table from #27 and adjusting the
AsyncPending
benchmark to use a simpler reusable awaiter.This is expected due to changing promises to structs and adding thread safety. But there are areas where we can improve the performance for pending promises.
ITreeHandleable
,IValueContainer
, andIMultiTreeHandleable
.Promise.Then
),MarkAwaited
is a virtual call, then the newPromiseRef
is created, thenHookupNewPromise
is a second virtual call. This was done for validation purposes, but it can be reduced to a single virtual call, with careful consideration for thePromiseRef
created when the call is invalid. Also note that we cannot use a generic creator passed into the virtual function, because AOT compilers have trouble with generic virtual functions and structs (and using a non-struct would still be a second virtual call and consume heap space).ITreeHandleable.Handle
is invoked,PromiseRef
calls a separate virtualExecute
function. This is done so that all calls toExecute
are wrapped in the try/catch, but that logic can be shifted to pass theIDelegate
structs to a generic function with the try/catch in it instead, similar to theCallbackHelper
for synchronous invokes.ValueContainer
s are created with 0 retains and retain is called when it is passed toResolveInternal
orRejectOrCancelInternal
. This can be changed to always create with 1 retain and don't call retain in those methods.PromiseRef.MaybeDispose()
will always callDispose()
virtually. Most of the timeMaybeDispose()
is called from the most derived class, so that can be changed to a direct call instead of virtual. [Edit] No longer true since Handle combine #55AsyncPromiseRef
directly to the awaitedPromiseRef
instead of creating anAwaiterRef
, by doing a special type check on the awaiter forPromiseAwaiter
in thePromiseMethodBuilder
. This optimization is trickier than the others and will need to make sure it doesn't box (very similar to Reporting progress from anasync Promise
function #10).ref local
andref return
features. This can be done while still supporting older language versions, but it will get ugly with the #ifdefs.IPromiseWaiter.AwaitOnCompletedInternal
directly in .Net Core instead of going throughAwaitOverrider
, since the JIT optimizes away the box instructions (but not in Unity).Unsafe.As<T>(object)
in .Net 5+Unfortunately, I don't think there is much that can be done for awaiting a resolved promise, as v1 was branchless (the promise was its own awaiter and didn't have to check itself for null). V2 is more complex with the struct wrapping a reference and using a separate
PromiseAwaiter
struct for safety. [Edit] Await promise was improved in #39.The text was updated successfully, but these errors were encountered: