C# & .NET: stackalloc

У меня есть несколько вопросов о функциональности stackalloc оператор.

  1. Как это на самом деле выделить? Я думал, что это что-то вроде:

    void* stackalloc(int sizeInBytes)
    {
        void* p = StackPointer (esp);
        StackPointer += sizeInBytes;
        if(StackPointer exceeds stack size)
            throw new StackruException(...);
        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 ответа

Решение

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

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

Редактировать:

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

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

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

    Однако в документе стандартов ECMA 335 - Common Language Infrastructure есть более точное описание:

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

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

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

Другие вопросы по тегам