34

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

У меня есть две 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 отладчиком на первом)
  • замерзает происходят ли построить я в режиме отладки или Release
  • замерзает, если я цель .NET 4.5 или .NET 4.6
  • Я попытался отключить RyuJIT, но это не влияет на замерзает

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

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

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

UPDATE 1: Добавление некоторые примеры кода для туманного объяснения выше ...

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

Теперь на моей машине код занимает произвольное количество времени. Кажется, от 5 до 30 минут. И, похоже, все хуже, чем больше коллекций Gen2, с которыми он сталкивается. Если я раскомментирую две линии настройки GC, она занимает 30-50 секунд и на машине А (так же, как на машине 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(); 
    } 
} 

UPDATE 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 по какой-то причине.

+4

Следующим шагом будет опубликовать [MCVE] (http://stackoverflow.com/help/mcve), поэтому мы можем попробовать воспроизводя это на нашей машине и посмотреть, не испытываем ли мы подобное поведение. Было ли это построено для работы как x86 или x64? –

+0

x64. Понял, работая над одним. Но его трудно получить, чтобы GC работали правильно. Я надеялся, что мне не хватает очевидного. –

+0

@MichaelCovelli Что происходит, когда вы вынуждаете GC в цикле использовать 'GC.Collect()'? – svick

ответ

5

Похоже, проблема была решена сейчас, см http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx

+0

Спасибо! Попробует попробовать после того, как исправление находится там и отчитается. –

+0

Это исправление: https://support.microsoft.com/en-us/kb/3088957 был только что выпущен и полностью устраняет проблему. –

+0

Версия для исправления отличается от версии Windows. Основываясь на комментарии от http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx, я считаю, что мы имеют следующее. Для Windows Vista, Windows 7, Windows Server 2008 и Windows Server 2008 R2: 3088957. Для Windows 8 и Windows Server 2012: 3088955. Для Windows 8.1 и Windows Server 2012 R2: 3088956. Для Windows 10: Нет исправления. –

26

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

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

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

Это дает лучшее понимание в том, что беспокоит программу от ступицы новой диагностики в VS2015. Это не займет много времени, так как только ядро ​​ делает любую работу, что легко сказать из использования ЦП. При случайных всплесках они длится недолго, совпадая с оранжевым знаком GC. Когда вы внимательно посмотрите на отметку GC, вы увидите, что это коллекция gen # 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.


UPDATE: GC ошибка подтверждена в this blog post. Исправьте скоро.


ОБНОВЛЕНИЕ: a hotfix was released.


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

+0

Спасибо, что посмотрели. Отметьте как ответ, если нет других после дня или двух. Я согласен, что этот пример легко оптимизировать. Я просто играл с ненужными выделениями, пока не получил что-то, чтобы продемонстрировать, что я вижу в своем коде. Разница между .NET 4.5 и 4.6 - вот что меня больше всего удивляет. Сообщите об этой проблеме на сайте connect.microsoft.com. Благодаря! –

+2

@MichaelCovelli Пожалуйста, разместите ссылку Microsoft Connect здесь, как только вы сообщили об этом, чтобы мы могли отслеживать эту проблему. – cremor

+2

Опубликовано в https://connect.microsoft.com/VisualStudio/feedback/details/1621480 –

10

Мы (и другие пользователи) столкнулись с подобной проблемой. Мы работали над этим, отключив фоновый 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> 
+0

Спасибо! Попробуем. Конечно, звучит как одна и та же проблема. –

+0

Оба этих решения обходят это. Переключение на сервер GC использует больше памяти, но при этом время выполнения на моей машине сокращается до 5 секунд. Установка gcConcurrent на false делает приложение занятием около 10 секунд - столько же, сколько он использовал в .NET 4.5 в VS2013. –