web-dev-qa-db-ja.com

ガベージコレクションとParallel.ForEachのVS2015アップグレード後の問題

自分のRのようなC#DataFrameクラスで数百万のデータ行を処理するコードがあります。データ行を並列に反復処理するためのParallel.ForEach呼び出しがいくつかあります。このコードは、VS2013と.NET4.5を使用して1年以上問題なく実行されています。

2台の開発マシン(AとB)があり、最近マシンAをVS2015にアップグレードしました。約半分の時間、コードの奇妙な断続的なフリーズに気づき始めました。長時間実行すると、最終的にコードが終了することがわかります。 1〜2分ではなく15〜120分かかります。

VS2015デバッガーを使用してすべてを壊そうとすると、何らかの理由で失敗し続けます。そこで、たくさんのログステートメントを挿入しました。このフリーズは、Parallel.ForEachループ中にGen2コレクションがある場合に発生することがわかりました(各Parallel.ForEachループの前後のコレクション数を比較)。余分な13〜118分全体が、Parallel.ForEachループ呼び出しがGen2コレクション(存在する場合)とオーバーラップする場合に費やされます。 Parallel.ForEachループ中にGen2コレクションがない場合(実行時の約50%)、すべてが1〜2分で正常に終了します。

マシンAのVS2013で同じコードを実行すると、同じフリーズが発生します。マシンB(アップグレードされたことはありません)でVS2013のコードを実行すると、完全に機能します。それは凍結することなく一晩何十回も実行されました。

私が気づいた/試したいくつかのこと:

  • フリーズは、マシンAにデバッガーが接続されているかどうかに関係なく発生します(最初はVS2015デバッガーを使用したものだと思いました)
  • デバッグモードとリリースモードのどちらでビルドしても、フリーズが発生します
  • .NET4.5または.NET4.6をターゲットにすると、フリーズが発生します
  • RyuJITを無効にしてみましたが、フリーズには影響しませんでした

デフォルトのGC設定はまったく変更していません。 GCSettingsによると、すべての実行はLatencyModeInteractiveとIsServerGCをfalseとして実行されています。

Parallel.ForEachを呼び出す前に、LowLatencyに切り替えることもできますが、何が起こっているのかを理解したいと思います。

VS2015のアップグレード後にParallel.ForEachで奇妙なフリーズを見た人はいますか?良い次のステップは何かについてのアイデアはありますか?

更新1:上記のあいまいな説明にサンプルコードを追加しています...

これがこの問題を実証することを願っているいくつかのサンプルコードです。このコードは、Bマシンで一貫して10〜12秒で実行されます。多くのGen2コレクションに遭遇しますが、それらはほとんど時間がかかりません。 2つのGC設定行のコメントを解除すると、Gen2コレクションがないように強制できます。 30〜50秒より少し遅くなります。

私のAマシンでは、コードにランダムな時間がかかります。 5分から30分の間のようです。そして、それが遭遇するGen2コレクションが増えるほど、悪化するようです。 2つのGC設定行のコメントを外すと、マシンAでも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();
    }
}

更新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が記載されています。

34
Michael Covelli
5
mpeac

これは確かにパフォーマンスが極端に悪く、バックグラウンドGCはここであなたが好むことをしていません。最初に気付いたのは、Parallel.ForEach()が使用しているタスクが多すぎることです。スレッドプールマネージャーは、スレッドの動作を「I/Oによって停止した」と誤解し、余分なスレッドを開始します。これは問題を悪化させます。そのための回避策は次のとおりです。

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

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

これにより、VS2015の新しい診断ハブからプログラムを苦しめるものについてのより良い洞察が得られます。 singleコアだけが作業を行うのにそれほど時間はかからず、CPU使用率から簡単にわかります。時折スパイクが発生しますが、オレンジ色のGCマークと一致して、それほど長くは続きません。 GCマークを詳しく見ると、gen#1コレクションであることがわかります。 veryの長い時間、私のマシンでは約6秒かかります。

もちろん、第1世代のコレクションはそれほど長くはかかりません。ここで発生しているのは、バックグラウンドGCがジョブを終了するのを待機している第1世代のコレクションです。つまり、実際には6秒かかるのはバックグラウンドGCです。バックグラウンドGCは、第0世代と第1世代のセグメントのスペースが十分に大きく、バックグラウンドGCがトラウンドしているときに第2世代のコレクションを必要としない場合にのみ有効です。このアプリの動作方法ではなく、非常に高い割合でメモリを消費します。小さなスパイクは、複数のタスクのブロックが解除され、配列を再度割り当てることができるようになることです。 gen#1コレクションがバックグラウンドGCを再び待機する必要がある場合、すぐに停止します。

注目すべきは、このコードの割り当てパターンがGCにとって非常に不親切であるということです。有効期間の長い配列(dr.DerivedValues)と有効期間の短い配列(tempArray)をインターリーブします。ヒープを圧縮するときにGCに多くの作業を与えると、割り当てられたすべての配列が移動することになります。

.NET 4.6 GCの明らかな欠陥は、バックグラウンドコレクションがヒープを効果的に圧縮していないように見えることです。前のコレクションがまったく圧縮されなかったかのように、何度も何度もジョブを実行しているように見えます。これが設計によるものなのかバグなのか見分けがつかないのですが、私はもうクリーンな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のバグが このブログ投稿 で確認されました。すぐに修正されます。


更新: 修正プログラムがリリースされました


更新:.NET4.6.1で修正

27
Hans Passant

私たち(および他のユーザー)も同様の問題に遭遇しました。アプリケーションのapp.configでバックグラウンドGCを無効にすることで、この問題を回避しました。 https://connect.Microsoft.com/VisualStudio/Feedback/Details/1594775 のコメントの説明を参照してください。

gcConcurrentのapp.config(非並行ワークステーション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>
10
ngm