Подтвердить что ты не робот

Сбор мусора и Parallel.ForEach Проблема после обновления VS2015

У меня есть код для обработки нескольких миллионов строк данных в моем собственном R-подобном классе С# DataFrame. Там несколько Parallel.ForEach вызывают параллельную итерацию строк данных. Этот код работает более года, используя VS2013 и .NET 4.5 без проблем.

У меня есть две dev-машины (A и B) и недавно обновленная машина A до VS2015. Я начал замечать странное прерывистое замораживание в моем коде примерно в половине случаев. Позволяя ему работать в течение длительного времени, оказывается, что код в конечном итоге заканчивается. Это займет всего 15-120 минут вместо 1-2 минут.

Попытки разбить все, используя отладчик VS2015, по какой-то причине не работают. Поэтому я вставил кучу операторов журнала. Оказывается, это замораживание происходит, когда есть коллекция Gen2 во время цикла Parallel.ForEach(сравнивая счетчик коллекции до и после каждого цикла Parallel.ForEach). Все дополнительные 13-118 минут тратятся внутри того, что Parallel.ForEach цикл звонит, случается, перекрываться с коллекцией Gen2 (если есть). Если нет коллекций Gen2 во время любых циклов Parallel.ForEach(около 50% времени, когда я запускаю его), все заканчивается отлично через 1-2 минуты.

Когда я запускаю тот же код в VS2013 на машине A, я получаю такие же зависания. Когда я запускаю код в VS2013 на машине B (который никогда не обновлялся), он работает отлично. Он провел десятки раз в ночное время без замерзания.

Некоторые вещи, которые я заметил/попробовал:

  • Замораживание происходит с или без отладчика, подключенного к машине A (я понял, что это было что-то с отладчиком VS2015)
  • Замораживание происходит, если я создаю режим Debug или Release
  • Замедление происходит, если я нацелен на .NET 4.5 или .NET 4.6
  • Я попытался отключить RyuJIT, но это не повлияло на зависание

Я вообще не изменяю настройки GC по умолчанию. Согласно GCSettings, все прогоны происходят с LatencyMode Interactive и IsServerGC как false.

Я могу просто переключиться на LowLatency перед каждым вызовом Parallel.ForEach, но я бы предпочел понять, что происходит.

Кто-нибудь еще видел странные зависания в Parallel.ForEach после обновления VS2015? Любые идеи о том, какой был бы следующий следующий шаг?

ОБНОВЛЕНИЕ 1: добавление некоторого примера кода в туманное объяснение выше...

Вот пример кода, который, я надеюсь, продемонстрирует эту проблему. Этот код работает через 10-12 секунд на машине B, последовательно. Он сталкивается с рядом коллекций Gen2, но у них почти нет времени. Если я раскомментирую две строки настроек GC, я могу заставить ее не иметь коллекций Gen2. Он несколько медленнее, чем через 30-50 секунд.

Теперь на моей машине A код занимает произвольное количество времени. Кажется, от 5 до 30 минут. И, похоже, все хуже, чем больше коллекций Gen2, с которыми он сталкивается. Если я раскомментирую две линии настройки GC, она занимает 30-50 секунд и на машине A (так же, как на машине B).

Может потребоваться некоторое изменение в отношении количества строк и размера массива, чтобы это отображалось на другой машине.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime;    

public class MyDataRow
{
    public int Id { get; set; }
    public double Value { get; set; }
    public double DerivedValuesSum { get; set; }
    public double[] DerivedValues { get; set; }
}

class Program
{
    static void Example()
    {
        const int numRows = 2000000;
        const int tempArraySize = 250;

        var r = new Random();
        var dataFrame = new List<MyDataRow>(numRows);

        for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { Id = i, Value = r.NextDouble() });

        Stopwatch stw = Stopwatch.StartNew();

        int gcs0Initial = GC.CollectionCount(0);
        int gcs1Initial = GC.CollectionCount(1);
        int gcs2Initial = GC.CollectionCount(2);

        //GCSettings.LatencyMode = GCLatencyMode.LowLatency;

        Parallel.ForEach(dataFrame, dr =>
        {
            double[] tempArray = new double[tempArraySize];
            for (int j = 0; j < tempArraySize; j++) tempArray[j] = Math.Pow(dr.Value, j);
            dr.DerivedValuesSum = tempArray.Sum();
            dr.DerivedValues = tempArray.ToArray();
        });

        int gcs0Final = GC.CollectionCount(0);
        int gcs1Final = GC.CollectionCount(1);
        int gcs2Final = GC.CollectionCount(2);

        stw.Stop();

        //GCSettings.LatencyMode = GCLatencyMode.Interactive;

        Console.Out.WriteLine("ElapsedTime = {0} Seconds ({1} Minutes)", stw.Elapsed.TotalSeconds, stw.Elapsed.TotalMinutes);

        Console.Out.WriteLine("Gcs0 = {0} = {1} - {2}", gcs0Final - gcs0Initial, gcs0Final, gcs0Initial);
        Console.Out.WriteLine("Gcs1 = {0} = {1} - {2}", gcs1Final - gcs1Initial, gcs1Final, gcs1Initial);
        Console.Out.WriteLine("Gcs2 = {0} = {1} - {2}", gcs2Final - gcs2Initial, gcs2Final, gcs2Initial);

        Console.Out.WriteLine("Press Any Key To Exit...");
        Console.In.ReadLine();
    }

    static void Main(string[] args)
    {
        Example();
    }
}

ОБНОВЛЕНИЕ 2: просто чтобы переместить вещи из комментариев для будущих читателей...

Это исправление: https://support.microsoft.com/en-us/kb/3088957 полностью устраняет проблему. Я не вижу никаких проблем с медлительностью после применения.

Оказалось, что не имеет ничего общего с Parallel.ForEach, на мой взгляд, на основе этого: http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx, хотя в исправлении упоминается Parallel.ForEach по какой-либо причине.

4b9b3361

Ответ 2

Это действительно работает слишком плохо, фон GC не делает вас здесь. Первое, что я заметил, это то, что Parallel.ForEach() использует слишком много задач. Диспетчер threadpool неправильно интерпретирует поведение потока как "увязшего с помощью ввода-вывода" и запускает дополнительные потоки. Это делает проблему еще хуже. Обходной путь для этого:

var options = new ParallelOptions();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;

Parallel.ForEach(dataFrame, options, dr => {
    // etc..
}

Это дает лучшее представление о том, что заставляет программу из нового центра диагностики в VS2015. Это не займет много времени, так как только одно ядро ​​выполняет любую работу, что легко сказать из использования ЦП. При случайных всплесках они длится недолго, совпадая с оранжевым знаком GC. Когда вы более внимательно посмотрите на отметку GC, вы увидите, что это коллектив # 1. Принимая очень долгое время, около 6 секунд на моей машине.

Коллекция Gen # 1, конечно же, не так долго, что вы видите здесь, это коллекция №1 поколения, ожидающая завершения работы GC. Другими словами, это фактически фоновый GC, который занимает 6 секунд. Фоновый GC может быть эффективен только в том случае, если пространство в сегментах gen # 0 и gen # 1 достаточно велико, чтобы не требовать коллекцию gen # 2, в то время как фоновый GC trundling. Не то, как работает это приложение, оно очень быстро обретает память. Маленький шип, который вы видите, - это несколько задач, которые разблокируются, и они могут снова распределять массивы. Быстро перемолоть, когда коллектив № 1 должен снова ждать фоновый GC.

Примечательно, что шаблон распределения этого кода очень недружелюбен к GC. Он чередует долгоживущие массивы (dr.DerivedValues) с недолговечными массивами (tempArray). Предоставляя GC много работы, когда он уплотняет кучу, каждый отдельный выделенный массив будет в конечном итоге перемещаться.

Очевидный недостаток в GC 4.6 4.6 состоит в том, что фоновая коллекция никогда не кажется эффективно уплотняющей кучу. Похоже, что эта работа снова и снова повторяется, как будто предыдущая коллекция вообще не была компактной. Является ли это по дизайну или ошибке, трудно сказать, у меня больше нет чистой машины 4.5. Я, конечно, склоняюсь к ошибке. Вы должны сообщить об этой проблеме на сайте connect.microsoft.com, чтобы Microsoft взглянула на нее.


Обходной путь очень прост, все, что вам нужно сделать, - это предотвратить неудобный переход между длинными и короткими объектами. Что вы делаете, предварительно распределяя их:

    for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { 
        Id = i, Value = r.NextDouble(), 
        DerivedValues = new double[tempArraySize] });

    ...
    Parallel.ForEach(dataFrame, options, dr => {
        var array = dr.DerivedValues;
        for (int j = 0; j < array.Length; j++) array[j] = Math.Pow(dr.Value, j);
        dr.DerivedValuesSum = array.Sum();
    });

И, конечно, полностью отключив фоновый GC.


ОБНОВЛЕНИЕ: ошибка GC подтверждена в этом сообщении в блоге. Исправьте скоро.


UPDATE: исправлено исправление.


UPDATE: исправлено в .NET 4.6.1

Ответ 3

Мы (и другие пользователи) столкнулись с аналогичной проблемой. Мы работали над этим, отключив фоновый GC в приложении app.config. Пожалуйста, см. Обсуждение в комментариях https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775.

app.config для gcConcurrent (несовместимая рабочая станция GC)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
    </startup>
<runtime>
    <gcConcurrent enabled="false" />
</runtime>

Вы также можете переключиться на GC сервера, хотя, похоже, этот подход использует больше памяти (на ненасыщенной машине?).

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
    </startup>
<runtime>
    <gcServer enabled="true" />
</runtime>
</configuration>