web-dev-qa-db-ja.com

スタックサイズがデフォルトの50倍のスレッドを作成する場合の危険性は何ですか?

私は現在、非常にパフォーマンスが重要なプログラムに取り組んでおり、リソース消費の削減に役立つ可能性がある探索することにした1つのパスは、ほとんどのデータを移動できるようにワーカースレッドのスタックサイズを増やすことでした(float[]s)スタックにアクセスします( stackalloc を使用)。

read スレッドのデフォルトのスタックサイズは1 MBなので、すべてのfloat[]sスタックを約50倍(50 MBに)拡張する必要があります。

これは一般に「安全でない」と見なされ、推奨されないことを理解していますが、この方法に対して現在のコードをベンチマークした後、530%を発見しました処理速度の向上!そのため、さらに調査することなくこのオプションを単純に渡すことはできません。スタックをこのような大きなサイズに増やすことに関連する危険性(何が問題になる可能性がありますか)、およびそのような危険性を最小限に抑えるためにどのような予防措置を講じる必要がありますか?

私のテストコード、

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}
228
Sam

テストコードとサムを比較すると、私たちはどちらも正しいと判断しました。
ただし、さまざまなことについて:

  • メモリへのアクセス(読み取りと書き込み)は、と同じくらい高速ですスタック、グローバル、またはヒープ。
  • 割り当てただし、スタックで最も速く、ヒープで最も遅くなります。

stack <global <heapのようになります。 (割り当て時間)
技術的には、スタック割り当ては実際には割り当てではなく、ランタイムはスタックの一部(フレーム?)が配列用に予約されていることを確認するだけです。

ただし、これには注意が必要です。
次のことをお勧めします。

  1. 関数をleaveしない(たとえば、参照を渡す)ことのない配列を頻繁に作成する必要がある場合、スタックを使用すると大幅に改善されます。
  2. 配列をリサイクルできる場合は、できる限りいつでもリサイクルしてください!ヒープは、長期のオブジェクトストレージに最適な場所です。 (グローバルメモリを汚染することは素晴らしいことではありません。スタックフレームが消えることがあります)

:1.値型にのみ適用されます;参照型はヒープに割り当てられ、利益は0に減少します)

質問自体に答えるために:私は、どんな大規模スタックテストでもまったく問題に遭遇していません。
システムが不足している場合、スレッドを作成するときに関数呼び出しとメモリ不足に注意しなければ、考えられる唯一の問題はスタックオーバーフローです。

以下のセクションは私の最初の答えです。それは間違っているようで、テストは正しくありません。参照用にのみ保持されます。


私のテストでは、スタックで割り当てられたメモリとグローバルメモリは、配列で使用するためにヒープに割り当てられたメモリよりも少なくとも15%遅くなります(120%の時間がかかります)。

これは私のテストコードです 、これはサンプル出力です:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

.NET 4.5.1でi7 4700 MQを使用して、Windows 8.1 Pro(Update 1)でテストしました
x86とx64の両方でテストしましたが、結果は同じです。

Edit:すべてのスレッドのスタックサイズを201 MB、サンプルサイズを5,000万に増やし、反復を5に減らしました。
結果は上記と同じです。

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

ただし、スタックは実際には遅くなります

45
Vercas

処理速度が530%向上しました。

それが断然最大の危険です。ベンチマークには重大な問題があります。これを予測できない動作をするコードには、通常、どこかにひどいバグが隠されています。

.NETプログラムで大量のスタックスペースを消費することは、過度の再帰による場合を除き、非常に困難です。マネージメソッドのスタックフレームのサイズは、石で設定されます。メソッドの引数とメソッド内のローカル変数の単純な合計。 CPUレジスタに格納できるものを除いて、それらは非常に少ないので無視できます。

スタックサイズを増やしても何も達成されません。使用されないアドレススペースの束を予約するだけです。もちろんメモリを使用しないことによるパフォーマンスの向上を説明できるメカニズムはありません。

これは、ネイティブプログラム、特にCで記述されたプログラムとは異なり、スタックフレーム上の配列用のスペースを予約することもできます。スタックバッファオーバーフローの背後にある基本的なマルウェア攻撃ベクトル。 C#でも同様に、stackallocキーワードを使用する必要があります。あなたがそれをしているなら、明らかな危険は、そのような攻撃やランダムなスタックフレーム破損の影響を受ける安全でないコードを書かなければならないことです。バグの診断が非常に難しい。後のジッターにはこれに対する対策があります。NET4.0以降では、ジッターがスタックフレームに「Cookie」を置くコードを生成し、メソッドが戻ったときにまだ変更されていないかどうかをチェックします。発生した場合に事故を傍受したり報告したりすることなく、デスクトップに即座にクラッシュします。それは...ユーザーの精神状態にとって危険です。

オペレーティングシステムによって起動されたプログラムのメインスレッドには、デフォルトで1 MBのスタックがあり、x64を対象にプログラムをコンパイルすると4 MBになります。これを増やすには、ビルド後のイベントで/ binオプションを指定してEditbin.exeを実行する必要があります。通常、32ビットモードで実行する場合、プログラムの起動に問題が生じる前に最大500 MBを要求できます。もちろん、スレッドは、32ビットプログラムの場合、通常、90 MB程度の危険ゾーンをホバリングできます。プログラムが長時間実行され、以前の割り当てからアドレス空間が断片化されたときにトリガーされます。このエラーモードを取得するには、ギグで、アドレス空間の合計使用量がすでに高くなっている必要があります。

コードをトリプルチェックしてください。何か非常に間違っています。コードを明示的に記述して利用しない限り、より大きなスタックでx5の高速化を実現することはできません。常に安全でないコードが必要です。 C#でポインターを使用すると、より高速なコードを作成するためのコツが常にあります。配列の境界チェックは行われません。

28
Hans Passant

私は単にそれを予測する方法がわからないという予約を持っています-許可、GC(スタックをスキャンする必要がある)など-すべてが影響を受ける可能性があります。代わりに、アンマネージメモリを使用したいと思います。

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}
22
Marc Gravell

間違っている可能性のあることの1つは、そうする許可を取得できない可能性があることです。完全信頼モードで実行していない限り、フレームワークはより大きなスタックサイズの要求を無視します(MSDNのThread Constructor (ParameterizedThreadStart, Int32)を参照)

システムスタックサイズをこのような膨大な数に増やす代わりに、ヒープで反復と手動スタック実装を使用するようにコードを書き直すことをお勧めします。

8
PMF

JavaやC#などのJITとGCを使用したマイクロベンチマーク言語は、少し複雑になる可能性があるため、既存のフレームワークを使用することをお勧めします-Java offers mhf残念ながら私の知る限り、C#はこれらに近づくものを何も提供していません。ジョンスキートは this を書いています。ここで私は盲目的に最も重要なことを処理すると思います(ジョンは知っています)ウォーミングアップ後のテストあたり30秒は我慢が長すぎたので(5秒はすべきでした)、タイミングを少し調整しました。

最初の結果は、Windows 7 x64での.NET 4.5.1です。数字は、5秒で実行できる反復を示しているため、高いほど優れています。

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT(ええ、それはまだちょっと悲しいです):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

これにより、最大14%のはるかに合理的な高速化が得られます(オーバーヘッドのほとんどはGCの実行によるものであり、現実的には最悪のシナリオと考えてください)。ただし、x86の結果は興味深いものです。そこで何が起こっているのかは完全には明らかではありません。

コードは次のとおりです。

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}
6
Voo

高性能な配列は通常のC#配列と同じ方法でアクセスできますが、それが問題の始まりになる可能性があります。次のコードを検討してください。

float[] someArray = new float[100]
someArray[200] = 10.0;

範囲外の例外が予想され、これは要素200にアクセスしようとしているため完全に理にかなっていますが、最大許容値は99です。stackallocルートに移動すると、境界チェックのために配列をラップするオブジェクトはありません。以下は例外を示しません。

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

上の例では、100個のfloatを保持するのに十分なメモリを割り当てており、float値10を保持するために、このメモリの開始位置+ 200 * sizeof(float)から始まるsizeof(float)メモリ位置を設定しています。フロートにメモリを割り当てたため、そのアドレスに何を格納できるか誰にもわかりません。運がよければ、現在使用されていないメモリを使用した可能性がありますが、同時に他の変数の保存に使用された場所を上書きする可能性があります。要約するには:予測不可能なランタイム動作。

6
MHOOS

パフォーマンスの差が大きすぎるため、問題は割り当てにほとんど関係しません。アレイアクセスが原因である可能性があります。

関数のループ本体を逆アセンブルしました。

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

命令の使用状況を確認できます。さらに重要なのは、スローされる例外 ECMA仕様

stind.r4: Store value of type float32 into memory at address

スローされる例外:

System.NullReferenceException

そして

stelem.r4: Replace array element at index with the float32 value on the stack.

スローされる例外:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

ご覧のとおり、stelemは、配列範囲のチェックと型のチェックでより多くの作業を行います。ループ本体はほとんど何もしない(値を割り当てるだけ)ため、チェックのオーバーヘッドが計算時間を支配します。そのため、パフォーマンスは530%異なります。

また、これはあなたの質問にも答えます。危険は、配列の範囲と型のチェックがないことです。これは安全ではありません(関数宣言で述べたように; D)。

5
HKTonyLee

編集:(コードと測定の小さな変更により、結果に大きな変化が生じます)

まず、デバッガー(F5)で最適化されたコードを実行しましたが、それは間違っていました。デバッガーなしで実行する必要があります(Ctrl + F5)。第二に、コードは完全に最適化されている可能性があるため、オプティマイザーが測定に干渉しないようにコードを複雑にする必要があります。すべてのメソッドが配列内の最後の項目を返すようにしましたが、配列は異なる方法で設定されます。また、OPのTestMethod2には余分なゼロがあり、常に10倍遅くなります。

あなたが提供した2つに加えて、他のいくつかの方法を試しました。メソッド3にはメソッド2と同じコードがありますが、関数はunsafeとして宣言されています。方法4は、定期的に作成された配列へのポインターアクセスを使用しています。方法5では、Marc Gravellが説明しているように、アンマネージメモリへのポインターアクセスを使用しています。 5つのメソッドすべてが非常に類似した時間で実行されます。 M5が最速です(M1が2番目に近い)。最速と最遅の違いは約5%で、これは私が気にすることではありません。

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
4
Dialecticus