dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.29k stars 4.74k forks source link

Task.Delay 100x slower on mono than coreclr on macOS #57877

Open trympet opened 3 years ago

trympet commented 3 years ago

Description

On macOS, the overhead of Task.Delay is 100x higher on mono.

The behavior has been observed on: 2017 MacBook w/ Intel i5 x86 2020 Mac Mini w/ M1 arm64

Steps to reproduce:

  1. dotnet new macos (requires .NET 6) or create from VS template.
  2. Open AppDelegate.cs and add the following:

        public override void DidFinishLaunching(NSNotification notification)
        {
            // Insert code here to initialize your application
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        }
    
        public static async Task TaskBenchmark()
        {
            int i = 0;
            while(i++ < 10000) {
                await Task.Delay(1).ConfigureAwait(false);
            }
        }
  3. Open top, Activity Monitor, etc., and observe CPU usage of the process.

For reference, the coreclr equivalent is waay more performant.

  1. Create a new .NET 5 console app from a template.
  2. dotnet new globaljson; set SDK to 5.0.9.
  3. Add the following and build with dotnet build -c Release /p:UseAppHost=True

    public static void Main(string[] args)
    {
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        _ = Task.Run(TaskBenchmark);
        Console.ReadKey();
    }
    
    public static async Task TaskBenchmark()
    {
        int i = 0;
        while(i++ < 10000) {
            await Task.Delay(1);
        }
    }

This completely breaks benign methods which rely on TPL for frequent polling.

Expected behavior

The CPU time should be equivalent or better on Mono than on coreclr.

Actual behavior

CPU time is much greater than what is reasonable for a wait method, causing hangs and freezes.

Configuration

OS=macOS Big Sur 11.5.2 (20G95) [Darwin 20.6.0] Apple M1 2.40GHz, 1 CPU, 8 logical and 8 physical cores Intel Core i5-7Y54 CPU 1.20GHz (Max: 1.30GHz) (Kaby Lake), 1 CPU, 4 logical and 2 physical cores .NET SDK=5.0.400

Data

Coreclr: image

Mono: image

dotnet-issue-labeler[bot] commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

EgorBo commented 3 years ago

I'd guess it's not Task.Delay that is slow but the machinery around async/await (state machines) handling

trympet commented 3 years ago

I'd guess it's not Task.Delay that is slow but the machinery around async/await (state machines) handling

Yes, sounds right. Using a plain old System.Threading.Timer provides acceptable perf.

However, the issue still remains; libraries which uses async await over explicit threading (e.g. render timers, async logging, etc.) can have widely different perf characteristics between Mono and coreclr. Having to fork every library which suffers because of this is less than ideal when multi-targeting.

SamMonoRT commented 3 years ago

@lambdageek @vargaz - marked for 7.0, but if there is a low risk fix, we can backport