Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Very poor performance of async task run on threadpool in .Net native

Tags:

I've observed a strange difference in managed vs .Net native code. I've a heavy job redirected to threadpool. When running the app in managed code, everything works smooth but as soon as I switch on native compilation - the task run few times slower and so slow that it hangs UI thread (I guess CPU is so overloaded).

Here are two screenshots from debug output, the one on the left is from managed code, and the one on the right is from native compilation. As you can see the time consumed by UI task is nearly the same in both cases, up to a time when threadpool job is started - then in managed version UI elapsed time grows (in fact UI gets blocked and you cannot take any action). Timings of threadpool job speak for themselves.

ManagedNative

The sample code to reproduce the problem:

private int max = 2000;
private async void UIJob_Click(object sender, RoutedEventArgs e)
{
    IProgress<int> progress = new Progress<int>((p) => { MyProgressBar.Value = (double)p / max; });
    await Task.Run(async () => { await SomeUIJob(progress); });
}

private async Task SomeUIJob(IProgress<int> progress)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < max; i++)
    {
        if (i % 100 == 0) { Debug.WriteLine($"     UI time elapsed => {watch.ElapsedMilliseconds}"); watch.Restart(); }
        await Task.Delay(1);
        progress.Report(i);
    }
}

private async void ThreadpoolJob_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Firing on Threadpool");
    await Task.Run(() =>
   {
       double a = 0.314;
       Stopwatch watch = new Stopwatch();
       watch.Start();
       for (int i = 0; i < 50000000; i++)
       {
           a = Math.Sqrt(a) + Math.Sqrt(a + 1) + i;
           if (i % 10000000 == 0) { Debug.WriteLine($"Threadpool -> a value = {a} got in {watch.ElapsedMilliseconds} ms"); watch.Restart(); };
       }
   });
    Debug.WriteLine("Finished with Threadpool");
}

If you need a complete sample - then you can download it here.

As I've tested the difference appears on both optimized/non optimized code, in both debug and release versions.

Does anybody have an idea what can cause the problem?

like image 275
Romasz Avatar asked Jan 07 '16 22:01

Romasz


People also ask

Is ThreadPool async?

The thread-based asynchronous programming approach, also called “work-stealing” or “bulkheading”, allows one thread pool to hand over a task to another thread pool (let's call it a work thread pool) and be notified to handle the result when the worker thread pool is done with the task.

Does Task use ThreadPool C#?

By default, TPL types like Task and Task<TResult> use thread pool threads to run tasks. You can also use the thread pool by calling ThreadPool.

What happens when thread pool is full C#?

By default, the MaxThreads of the ThreadPool is very high. Usually you'll never get there, your app will crash first. So when all threads are busy the new tasks are queued and slowly, at most 1 per 500 ms, the TP will allocate new threads.

Does async await use thread pool?

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active.


1 Answers

This issue is caused because the “ThreadPool” math loop is causing GC starvation. Essentially, the GC has decided that it needs to run (due to wanting to do some interop allocation) and it’s trying to stop all of the threads to do collection/compaction. Unfortunately, we haven’t added the ability for .NET Native to hijack hot loops like the one you have below. This is briefly mentioned on Migrating Your Windows Store App to .NET Native page as:

Infinite looping without making a call (for example, while(true);) on any thread may bring the app to a halt. Similarly, large or infinite waits may bring the app to a halt.

One way to work around this is to add a call site into your loop (the GC is very happy to interrupt your thread when it’s trying to call another method!).

    for (long i = 0; i < 5000000000; i++)
           {
               MaybeGCMeHere(); // new callsite
               a = Math.Sqrt(a) + Math.Sqrt(a + 1) + i;
               if (i % 1000000000 == 0) { Debug.WriteLine($"Threadpool -> a value = {a} got in {watch.ElapsedMilliseconds} ms"); watch.Restart(); };
    }

...

    [MethodImpl(MethodImplOptions.NoInlining)] // need this so the callsite isn’t optimized away
    private void MaybeGCMeHere()
    {
    }

The downside is that you’ll have this “ugly” looking hack and you may suffer a bit from the added instructions. I've let some folks here know that this thing that we assumed was "vanishingly rare" is actually hit by a customer and we'll see what can be done about it.

Thanks for the report!

Update: We have made some big improvements around this scenario and will be able to hijack most long running threads for GC. These fixes will be available in the Update 2 set of UWP tools out probably in April? (I don't control the shipping schedule :-) )

Update update: The new tools are now available as part of UWP tools 1.3.1. We don't expect to have a perfect solution to threads aggressively fighting against being hijacked by the GC but I expect this scenario to be much better with the latest tools. Let us know!

like image 61
MattWhilden Avatar answered Sep 25 '22 12:09

MattWhilden