在 .Net Native 中,线程池上异步任务的性能非常差

21

我发现了托管代码和 .Net Native 代码之间的一个奇怪差异。我有一个严重的作业被重定向到线程池。在托管代码中运行应用程序时,一切都很顺利,但是一旦我切换到本地编译,任务运行速度变慢了几倍,以至于它会挂起UI线程(我猜CPU过载了)。下面是来自调试输出的两个截图,左边的是托管代码,右边的是本地编译。您可以看到在两种情况下UI任务消耗的时间几乎相同,直到线程池作业开始的时间为止-然后在托管版本中,UI经过时间增加(实际上UI被阻塞,您无法采取任何行动)。线程池作业的计时说明了问题所在。

ManagedNative

复现问题的示例代码:

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");
}
如果您需要完整的示例-那么您可以在这里下载
我已经测试了优化和未优化的代码,无论是Debug版还是Release版都存在差异。
有人知道是什么原因导致了这个问题吗?

2
可能需要查看生成的IL代码和机器码。 - Rob
4
我在.NET Native编译器和运行时团队工作。我们通常使用PerfView进行此类调查。如果您可以收集一些ETL跟踪数据(一个带有.NET Native和另一个没有),并将它们发送给我们(dotnetnative@microsoft.com),我们会让某人来看一下。 - MattWhilden
1
可能是线程池饥饿了。你有尝试过使用 ThreadPool.SetMinThreads/SetMaxThreads 吗? - noseratio - open to work
2
@Noseratio 在UWP中似乎没有控制线程数量的选项。 - Romasz
1
@MattWhilden 我已经发送了一封电子邮件到您提供的地址。我观察到这主要是针对部署在ARM设备上的应用程序 - 是否可能为此类进程运行perview? - Romasz
1个回答

14
这个问题是因为“ThreadPool”数学循环导致GC饥饿引起的。实际上,由于需要进行Interop分配,GC已经决定需要运行,并试图停止所有线程来进行回收/压缩。不幸的是,我们还没有添加.NET Native接管像下面这样的热循环的能力。如此简要地提到了如下内容:将您的Windows商店应用程序迁移到.NET Native 页面:

在任何线程上无限循环而不进行调用(例如while(true);)可能会使应用程序停止。同样,大型或无限等待也可能导致应用程序停止。

解决这个问题的一种方法是在循环中添加调用站点(当尝试调用另一个方法时,GC非常高兴打断您的线程!)。

    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()
    {
    }

缺点是你会有一个“丑陋”的hack,而且可能会因为增加的指令而受到一些影响。我已经让这里的一些人知道了,我们认为这个问题是“极其罕见”的,但实际上有客户遇到了这个问题,我们将看看能做些什么。
感谢您的报告!
更新:我们在这种情况下进行了一些重大改进,并将能够劫持大多数长时间运行的线程以进行GC。这些修复程序将在UWP工具的Update 2中提供,可能在4月份左右发布?(我不能控制发货时间表 :-))
更新更新:新工具现在作为UWP工具1.3.1的一部分可用。我们不希望对抗被GC劫持的线程有一个完美的解决方案,但我希望这种情况在最新工具下会更好。让我们知道!

2
感谢整个团队的关注。你们中的一位同事说,这将在下一个VS更新中得到纠正 - 这太棒了。我同意在桌面上复现此问题更难,但在ARM上我认为这是一个真实的场景 - 实际上我在我的应用程序中观察到了这一点。我有一个处理照片并对像素进行一些数学运算的方法,由于它消耗CPU,因此被重定向到线程池,这就是我发现问题的地方。再次感谢您。 - Romasz
1
我也稍微编辑了一下你的回答,并将MSDN链接加粗 - 这可能有助于某些人节省时间。 - Romasz
1
编辑得很棒!我的SO标记语言不太好,所以我非常感激!似乎我们会在第二个更新中修复这种问题。 - MattWhilden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接