Очень низкая производительность асинхронной задачи, выполняемой в пуле потоков в нативном .Net.

Я заметил странную разницу между управляемым и собственным кодом .Net. У меня тяжелая работа, перенаправленная в пул потоков. При запуске приложения в управляемом коде все работает гладко, но как только я включаю нативную компиляцию - задача выполняется в несколько раз медленнее и настолько медленно, что зависает поток пользовательского интерфейса (я думаю, процессор настолько перегружен).

Вот два снимка экрана с выходными данными отладки: один слева — из управляемого кода, а справа — из собственной компиляции. Как вы можете видеть, время, затраченное на задачу пользовательского интерфейса, почти одинаково в обоих случаях, вплоть до момента запуска задания пула потоков - затем в управляемой версии истекшее время пользовательского интерфейса увеличивается (фактически пользовательский интерфейс блокируется, и вы не можете предпринять какие-либо действия). Время работы threadpool говорит само за себя.

«Управляемый»Родной

Пример кода для воспроизведения проблемы:

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");
}

Если вам нужен полный образец, вы можете загрузить его здесь< /а>.

Как я тестировал, разница проявляется как в оптимизированном, так и в неоптимизированном коде, как в отладочной, так и в релизной версиях.

Кто-нибудь знает, что может вызвать проблему?


person Romasz    schedule 07.01.2016    source источник
comment
Возможно, придется взглянуть на испускаемый IL и машинный код.   -  person Rob    schedule 08.01.2016
comment
Я работаю в команде .NET Native Compiler и Runtime. Мы обычно используем PerfView для таких исследований. Если вы сможете собрать несколько трассировок etl (одну с .net native и одну без) и отправить их нам ([email protected]), мы попросим кого-нибудь взглянуть на них.   -  person MattWhilden    schedule 08.01.2016
comment
Может быть голодание пула потоков. Вы играли с ThreadPool.SetMinThreads/SetMaxThreads?   -  person noseratio    schedule 08.01.2016
comment
@Noseratio Похоже, что в UWP нет возможности контролировать количество потоков.   -  person Romasz    schedule 08.01.2016
comment
@MattWhilden Я отправил электронное письмо на указанный вами адрес. Я наблюдал это в основном для приложения, развернутого на устройстве ARM - можно ли запустить perview для такого процесса?   -  person Romasz    schedule 08.01.2016


Ответы (1)


Эта проблема вызвана тем, что математический цикл «ThreadPool» вызывает голодание сборщика мусора. По сути, сборщик мусора решил, что ему нужно запуститься (из-за желания выполнить некоторое распределение между операциями), и он пытается остановить все потоки для выполнения сбора/сжатия. К сожалению, мы не добавили в .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()
    {
    }

Недостатком является то, что у вас будет этот «уродливый» вид, и вы можете немного пострадать от добавленных инструкций. Я дал знать некоторым людям, что эта вещь, которую мы считали «исчезающе редкой», на самом деле попала под удар клиента, и мы посмотрим, что можно с этим сделать.

Спасибо за отчет!

Обновление: мы внесли значительные улучшения в этот сценарий и теперь сможем перехватывать самые длительные потоки для GC. Эти исправления будут доступны в наборе инструментов UWP Update 2, который, вероятно, выйдет в апреле? (Я не контролирую график доставки :-))

Обновление обновления: новые инструменты теперь доступны как часть инструментов UWP 1.3.1. Мы не ожидаем идеального решения для потоков, агрессивно борющихся с захватом GC, но я ожидаю, что этот сценарий будет намного лучше с новейшими инструментами. Дайте нам знать!

person MattWhilden    schedule 12.01.2016
comment
Спасибо всей команде за то, что позаботились об этом. Один из ваших коллег сказал, что это будет исправлено в ближайшем обновлении VS - это здорово. Я согласен, что мне было сложнее воспроизвести проблему на рабочем столе, но на ARM я думаю, что это реальный сценарий - на самом деле я наблюдал это в своем приложении. У меня есть метод, который работает с фотографиями и выполняет некоторые математические операции с пикселями, так как он потребляет процессор, он перенаправляется в пул потоков, вот где я заметил проблему. Спасибо еще раз. - person Romasz; 12.01.2016
comment
Я также немного отредактировал ваш ответ и сделал ссылку MSDN жирной - есть шанс, что это может помочь кому-то сэкономить время. - person Romasz; 12.01.2016
comment
Редакции отличные! Моя разметка SO не очень хороша, поэтому я очень ценю это! Похоже, что в обновлении 2 у нас будут некоторые исправления для подобных вещей. - person MattWhilden; 12.01.2016