3 ms·
It would be interesting to see how much the C# memory usage would go down if you allocated a single Task.Delay at the start and had all the tasks wait for it. I
by kevingadd 3y ago
It would be interesting to see how much the C# memory usage would go down if you allocated a single Task.Delay at the start and had all the tasks wait for it. It would probably result in it finishing in 10sec instead of climbing up to 12, while still running 1 million tasks.
I'm not sure how you would do this in other runtimes, and there is still a risk of new problems cropping up.
1 million Task.Delays means 1 million queued waits in the sleep machinery, which could run into some bad scaling and overhead. But then 1 million tasks waiting on a single awaitable probably hits overhead in other subsystems. My guess would be that 1 million waits is much cheaper than 1 million sleeps, because sleeps have to be sorted by due time while the list of tasks waiting for completion just needs to be sequential. Entries in the sleep queue are also going to be bigger (at minimum, 4-8 bytes for due time + 4-8 bytes per completion callback) while entries in a completion queue would just be 4-8 bytes for the completion callback.
Task.WhenAll also has potentially worse behavior than the other apps if you care about memory, since it will need to allocate a big array of 1 million Tasks in order to pass it in. Many of the other programs instead loop over the tasks, which eliminates that overhead.
- JB_Dev 3y agoThe other thing I noticed is that they are wrapping every Task.Delay(…) inside a Task.Run(…) call. That should not be necessary. The inner task will yield back to the called when it hits the delay call anyway. There is no need to queue it with an immediate yield as if it is background or cpu-bound work. The Run() call might be creating a more complex state machine and using more memory than necessary (2 tasks vs just 1 for each delay). The memory usage might be even lower if the Run() call is dropped.
- dodyg 3y agoYup - this would do the job faster and better. Task task = Task.Delay(TimeSpan.FromSeconds(1)); tasks.Add(task);
- JB_Dev 3y agoI was sufficiently curious and just went and tested this using BenchmarkDotNet. The example code is here (https://github.com/J-Bax/CSharpBenchmarkExamples https://github.com/J-Bax/CSharpBenchmarkExamples). The difference is quite significant. (1) With the authors code (using Task.Run), I get ~428MB of allocations. (2) Dropping the unnecessary Task.Run(...), I get ~183MB of allocations. (3) Doing (2) and waiting N times on the same delay, I get ~39MB of allocations. This was all using .NET 6 too. .NET 7 or the 8 preview might be even better since they are working hard on performance in recent releases. So even looking at just (2), that puts .NET on par with the rust library.