perf: specialize Task.WhenAny for 3 and 4 tasks#127216
perf: specialize Task.WhenAny for 3 and 4 tasks#127216unsafePtr wants to merge 1 commit intodotnet:mainfrom
Conversation
Mirrors the existing TwoTaskWhenAnyPromise pattern to eliminate the Task[] defensive-copy allocation for Count=3 and Count=4. Count>=5 unchanged. Closes dotnet#126748
|
Tagging subscribers to this area: @dotnet/area-system-threading-tasks |
|
@dotnet-policy-service agree |
|
Do you have evidence that waiting for 3 or 4 tasks is common enough to make the extra code worth it? |
@jkotas went searching across widely-used .NET projects. Here's what I found:
return _executingTasks.Count switch
{
1 => _executingTasks[0].ExecutionTaskSafe!,
2 => Task.WhenAny(_executingTasks[0].ExecutionTaskSafe!, _executingTasks[1].ExecutionTaskSafe!),
_ => Task.WhenAny(_executingTasks.Select(v => v.ExecutionTaskSafe!))
};You are right there are not many places. I've used this tool with regex. 4task specialization is included because it follows the same pattern as 3-task at near-zero incremental cost. If you'd prefer to land 3task only, happy to narrow. Thank you, and I am sorry if I am being noisy 🙂 |
30 nanoseconds improvement in dotnet watch loop that runs once per edit is noise. The extra code is going to add much more than 30 nanoseconds to bootstrap, so just to break even the loop would have to run many times. |
|
@jkotas The actual motivation was reducing the ~50B per-call allocation (the internal Task[] defensive copy), not the 30ns. I understand that maintenance is a permanent cost and that's hard to justify without hot call site. I've run into this because we had 4 tasks with WhenAny in a hot loop, but we've since changed the flow to avoid WhenAny there altogether, so the original need is gone. Closing. Thanks for the review. |
Mirrors the existing TwoTaskWhenAnyPromise pattern to eliminate the Task[] defensive-copy allocation for Count=3 and Count=4. Count>=5 unchanged.
Closes #126748
Benchmarks
Benchmark code