C # & .NET: stackalloc - PullRequest
       61

C # & .NET: stackalloc

23 голосов
/ 12 декабря 2011

У меня есть несколько вопросов о функциональности оператора 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 замечает, что мы не используем массив, поэтому, может быть, он даже не выделяет его?

Ответы [ 2 ]

11 голосов
/ 12 декабря 2011

Случай, когда 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 дает выигрыш в производительности, этот вопрос, вероятно, ближе к большинству случаев, когда кто-то может охотно «оптимизировать» его использование. Надеемся, что два фрагмента кода вместе показывают, что целое число stackalloc может дать прирост, а также может сильно снизить производительность.

Как правило, вы даже не должны рассматривать stackalloc, если только вам не понадобится использовать закрепленную память для взаимодействия с неуправляемым кодом в любом случае, и это следует рассматривать как альтернативу fixed, а не альтернативу общему выделению кучи , Использование в этом случае все еще требует осторожности, предусмотрительности перед началом и профилирования после завершения.

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

Edit:

Чтобы ответить на часть 1 вопроса. Stackalloc концептуально очень много, как вы описываете. Он получает кусок памяти стека, а затем возвращает указатель на этот кусок. Он не проверяет, подходит ли память как таковая, но скорее если он попытается получить память в конец стека - который защищен .NET при создании потока - тогда это заставит ОС возвратить исключение во время выполнения , который затем превращается в управляемое исключение .NET. Примерно то же самое происходит, если вы просто выделяете один байт в методе с бесконечной рекурсией - если только вызов не был оптимизирован, чтобы избежать такого выделения стека (иногда это возможно), то один байт в конечном итоге будет суммироваться, чтобы вызвать исключение переполнения стека.

3 голосов
/ 12 декабря 2011
  1. Я не могу дать точный ответ, но stackalloc реализован с использованием кода операции IL localloc. Я посмотрел на машинный код, сгенерированный сборкой релиза для stackalloc, и он оказался более запутанным, чем я ожидал. Я не знаю, будет ли localloc проверять размер стека, как вы указываете своим if, или переполнение стека обнаруживается ЦП, когда аппаратный стек фактически переполняется.

    Комментарии к этому ответу указывают, что ссылка, предоставленная на localloc, выделяет место из "локальной кучи". Проблема в том, что нет хорошего онлайн-справочника по MSIL, за исключением фактического стандарта, доступного в формате PDF Ссылка выше взята из класса System.Reflection.Emit.OpCodes, который не относится к MSIL, а скорее к библиотеке для генерации MSIL.

    Однако в документе по стандартам ECMA 335 - Инфраструктура общего языка есть более точное описание:

    Частью каждого состояния метода является пул локальной памяти. Память может быть явно выделена из локального пула памяти с помощью инструкции localloc. Вся память в локальном пуле памяти освобождается при выходе из метода, и это единственный способ восстановления памяти в локальном пуле памяти (нет инструкции по освобождению локальной памяти, выделенной во время этого вызова метода). Пул локальной памяти используется для выделения объектов, тип или размер которых неизвестен во время компиляции и которые программист не хочет выделять в управляемой куче.

    Таким образом, в основном «пул локальной памяти» - это то, что иначе называется «стеком», а язык C # использует оператор stackalloc для выделения из этого пула.

  2. В сборке релиза оптимизатор достаточно умен, чтобы полностью удалить вызов на HeapAllocation, что приводит к гораздо меньшему времени выполнения. Кажется, что он не достаточно умен, чтобы выполнить ту же оптимизацию при использовании stackalloc. Если вы отключите оптимизацию или каким-либо образом используете выделенный буфер, вы увидите, что stackalloc немного быстрее.

...