23

stackallocオペレーターの機能についていくつか質問があります。

  1. 実際にどのように割り当てますか?私はそれが次のようなことをすると思った:

    void* stackalloc(int sizeInBytes)
    {
        void* p = StackPointer (esp);
        StackPointer += sizeInBytes;
        if(StackPointer exceeds stack size)
            throw new StackOverflowException(...);
        return p;
    }
    

    しかし、私はいくつかのテストを行いましたが、それがどのように機能するかはわかりません. それが何をどのように行うのかを正確に知ることはできませんが、基本を知りたいです。

  2. ヒープ割り当てよりもスタック割り当ての方が速いと思いました (まあ、確かにそう思います)。では、なぜこの例は:

     class Program
     {
         static void Main(string[] args)
         {
             Stopwatch sw1 = new Stopwatch();
             sw1.Start();
             StackAllocation();
             Console.WriteLine(sw1.ElapsedTicks);
    
             Stopwatch sw2 = new Stopwatch();
             sw2.Start();
             HeapAllocation();
             Console.WriteLine(sw2.ElapsedTicks);
         }
         static unsafe void StackAllocation()
         {
             for (int i = 0; i < 100; i++)
             {
                 int* p = stackalloc int[100];
             }
         }
         static void HeapAllocation()
         {
             for (int i = 0; i < 100; i++)
             {
                 int[] a = new int[100];
             }
         }
     }
    

スタック割り当ての平均結果は280~ ティック、ヒープ割り当ては通常1 ~ 0 ティックですか? (私のパソコンでは Intel Core i7)。

私が現在使用しているコンピューター (Intel Core 2 Duo) では、結果は以前のものよりも理にかなっています (おそらく、最適化コードが VS でチェックされていないため): スタック割り当てで 460~ ティック、ヒープ割り当てで約380 ティック

しかし、これはまだ意味がありません。なぜそうなのですか?CLR は配列を使用していないことを認識しているので、配列を割り当てさえしないのではないでしょうか?

4

2 に答える 2

11

stackalloc の方が速い場合:

 private static volatile int _dummy; // just to avoid any optimisations
                                         // that have us measuring the wrong
                                         // thing. Especially since the difference
                                         // is more noticable in a release build
                                         // (also more noticable on a multi-core
                                         // machine than single- or dual-core).
 static void Main(string[] args)
 {
     System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch();
     Thread[] threads = new Thread[20];
     sw1.Start();
     for(int t = 0; t != 20; ++t)
     {
        threads[t] = new Thread(DoSA);
        threads[t].Start();
     }
     for(int t = 0; t != 20; ++t)
        threads[t].Join();
     Console.WriteLine(sw1.ElapsedTicks);

     System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
     threads = new Thread[20];
     sw2.Start();
     for(int t = 0; t != 20; ++t)
     {
        threads[t] = new Thread(DoHA);
        threads[t].Start();
     }
     for(int t = 0; t != 20; ++t)
        threads[t].Join();
     Console.WriteLine(sw2.ElapsedTicks);
     Console.Read();
 }
 private static void DoSA()
 {
    Random rnd = new Random(1);
    for(int i = 0; i != 100000; ++i)
        StackAllocation(rnd);
 }
 static unsafe void StackAllocation(Random rnd)
 {
    int size = rnd.Next(1024, 131072);
    int* p = stackalloc int[size];
    _dummy = *(p + rnd.Next(0, size));
 }
 private static void DoHA()
 {
    Random rnd = new Random(1);
    for(int i = 0; i != 100000; ++i)
        HeapAllocation(rnd);
 }
 static void HeapAllocation(Random rnd)
 {
    int size = rnd.Next(1024, 131072);
    int[] a = new int[size];
    _dummy = a[rnd.Next(0, size)];
 }

このコードと質問のコードの重要な違い:

  1. いくつかのスレッドが実行されています。スタック割り当てでは、独自のスタックに割り当てています。ヒープ割り当てでは、他のスレッドと共有されているヒープから割り当てています。

  2. より大きなサイズが割り当てられます。

  3. 毎回異なるサイズが割り当てられます (テストをより決定論的にするために乱数発生器をシードしましたが)。これにより、ヒープの断片化が発生する可能性が高くなり、毎回同じ割り当てを行う場合よりもヒープ割り当ての効率が低下します。

これと同様に、を使用して配列をヒープに固定するstackalloc代わりによく使用されることも注目に値します。fixed配列のピニングはヒープのパフォーマンスに悪影響を及ぼします (そのコードだけでなく、同じヒープを使用する他のスレッドにとっても)。そのため、要求されたメモリが妥当な時間使用されている場合、パフォーマンスへの影響はさらに大きくなります。

私のコードstackallocはパフォーマンス上の利点をもたらすケースを示していますが、問題のそれはおそらく、誰かがそれを使用して熱心に「最適化」する可能性があるほとんどのケースに近いでしょう。願わくば、この 2 つのコードを組み合わせることで、全体stackallocが向上し、パフォーマンスが大幅に低下する可能性があることが示されます。

一般に、stackallocアンマネージ コードと対話するために固定メモリを使用する必要がない限り、考慮すべきではありませんfixed。また、一般的なヒープ割り当ての代替ではなく、代替と見なす必要があります。この場合の使用には、注意が必要であり、開始する前に事前に検討し、終了後にプロファイルを作成する必要があります。

他の場合に使用すると利点が得られる可能性がありますが、試してみるパフォーマンス向上のリストのはるか下にあるはずです。

編集:

質問のパート1に答える。Stackalloc は、概念的にはあなたが説明したとおりです。スタック メモリのチャンクを取得し、そのチャンクへのポインターを返します。メモリがそのように収まるかどうかはチェックしませんが、スレッドの作成時に.NETによって保護されているスタックの最後にメモリを取得しようとすると、OSが例外をランタイムに返します。これは、.NET 管理の例外に変わります。無限再帰を使用してメソッドに 1 バイトを割り当てるだけでも、ほとんど同じことが起こります。そのスタック割り当てを回避するように呼び出しが最適化されていない限り (場合によっては可能です)、1 バイトは最終的にスタック オーバーフロー例外をトリガーするのに十分な量になります。

于 2011-12-12T12:13:08.970 に答える
3
  1. 正確な回答はできませんがstackalloc、IL opcode を使用して実装されていlocallocます。のリリース ビルドによって生成されたマシン コードを見たところ、stackalloc予想以上に複雑でした。locallocあなたが示したようにスタックサイズをチェックするのか、ifそれともハードウェアスタックが実際にオーバーフローしたときにCPUによってスタックオーバーフローが検出されるのかはわかりません。

    この回答へのコメントは、提供されたリンクがlocalloc「ローカルヒープ」からスペースを割り当てることを示しています。問題は、PDF 形式で入手できる実際の標準以外に、MSIL の適切なオンライン リファレンスがないことです。上記のリンクは、System.Reflection.Emit.OpCodesMSIL に関するものではなく、MSIL を生成するためのライブラリであるクラスからのものです。

    ただし、標準ドキュメントECMA 335 - Common Language Infrastructureには、より正確な説明があります。

    各メソッド状態の一部は、ローカル メモリ プールです。locallocメモリは、命令を使用してローカル メモリ プールから明示的に割り当てることができます。ローカル メモリ プール内のすべてのメモリは、メソッドの終了時に回収されます。これが、ローカル メモリ プール メモリを回収する唯一の方法です (このメソッドの呼び出し中に割り当てられたローカル メモリを解放するための命令は提供されていません)。ローカル メモリ プールは、コンパイル時に型やサイズが不明で、プログラマがマネージ ヒープに割り当てたくないオブジェクトを割り当てるために使用されます。

    したがって、基本的に「ローカル メモリ プール」は「スタック」とも呼ばれるものであり、C# 言語はstackalloc演算子を使用してこのプールから割り当てます。

  2. リリース ビルドでは、オプティマイザは への呼び出しを完全に削除するほどスマートなのでHeapAllocation、実行時間が大幅に短縮されます。を使用するときに同じ最適化を実行するほどスマートではないようですstackalloc。最適化をオフにするか、何らかの方法で割り当てられたバッファを使用すると、stackallocわずかに高速になることがわかります。

于 2011-12-12T11:18:39.267 に答える