Привет всем, в данной статье я приведу несколько способов создания указателей в C#.

Указатели необходимы к примеру ,для взаимодействия с библиотеками, использующими ручное выделение/удаление блоков памяти.


Способы создания указателей

stackalloc

fixed

Marshal

Первым из них является stackalloc.

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

Ниже приведу фрагмент кода, создающего указатель с помощью stackalloc.

private static void Malloc(int Length)
        {
            unsafe
            {
                bool* n = stackalloc bool[Length];
            }
        }

Однако, стоит отметить, что выделение памяти имеет ограничение.

Так, попытка выделения в 64битном приложение , массива bool длиной 5 мегабайт (5 242 880 байт), создала StackOverflowExection .

В специфекации stackalloc ,отсутствует максимальный объем выделяемой памяти, но говорится следующее :

Объем доступной памяти в стеке ограничен. При выделении слишком большого объема памяти в стеке возникает исключение StackOverflowException

Поскольку объем доступной памяти на стеке зависит от среды, в которой выполняется код, при определении фактического предельного значения следует использовать консервативное значение. Старайтесь не использовать stackalloc в циклах. Выделяйте блок памяти за пределами цикла и используйте его повторно внутри цикла.

В целом, stackalloc , полезен для ситуаций , когда не требуется выделять большой блок памяти.


Следующим способом создания указателя , является fixed .

Оператор fixed задает указатель на управляемую переменную и "закрепляет" эту переменную во время выполнения оператора. Указатели на перемещаемые управляемые переменные полезны только в контексте fixed. Без контекста fixed при сборке мусора эти переменные могут переноситься непредсказуемым образом. Компилятор C# позволяет присвоить указатель только управляемой переменной в операторе fixed.

Оператор fixed не позволяет сборщику мусора переносить перемещаемую переменную. Оператор fixed допускается только в небезопасном контексте. Можно также использовать ключевое слово fixed для создания буферов фиксированного размера.

Ниже приведу фрагмент кода , использующего оператор fixed для создания указателя :

static void Copy(int[]m,out int[]n,int offset,int Length)
        {
            n = new int[Math.Min(m.Length - offset, Length)];
            unsafe
            {
                fixed(int* _m = m, _n = n)
                {

                   
                    Copy(_m, _n, offset, n.Length);
                }
            }
        }
        static unsafe void Copy(int *_inp,int *_out,int offset,int Length)
        {
            Parallel.For(0, Length, r => {
                _out[r] = _inp[r + offset];
            });
        }

Сразу отмечу, что к указателю , созданному в блоке fixed, нельзя получить доступ из Parallel.For. Также, указатель созданный в блоке fixed, нельзя приравнивать к другому указателю. Если указатели относятся к 1 типу данных, их можно поместить в 1 оператор fixed.


Ещё одним способом создания указателя является библиотечный класс Marshal. Данный класс находится в пространстве имен System.Runtime.InteropServices.

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

В данном классе присутствуют 2 метода, позволяющих выделять память под указатели.

FreeHGlobal(IntPtr)

Освобождает память, выделенную ранее из неуправляемой памяти процесса.

AllocHGlobal(Int32)

Выделяет память из неуправляемой памяти процесса, используя заданное количество байтов.

FreeCoTaskMem(IntPtr)

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

AllocCoTaskMem(Int32)

Выделяет блок памяти указанного размера из механизма распределения памяти для задач COM.

В случае использования AllocHGlobal и AllcCoTaskMem выделенную память необходимо очистить с помощью методов FreeHGlobal и FreeCoTaskMem соответственно. AllocHGlobal способен выделить до 2 гигабайт памяти, AllcCoTaskMem в отличии от него, способен работать и с большим объемом.

Всем спасибо за внимание.

Комментарии (10)


  1. BkmzSpb
    15.08.2021 22:27
    +17

    Давайте я побуду тем самым человеком, который первым оставляет длинную критическую простыню под статьей.

    Я не уверен, что название статьи соответствует ее содержанию. Что вы подразумеваете под "созданием указателей"? Создать указатель несложно:

    byte* ptr = (byte*)42;, выполненное в unsafe-контексте.

    Может быть вы имели ввиду IntPtr/UIntPtr? Чем не указатели? Создать можно вообще оператором new.

    Еще есть "управляемые" (managed) указатели (или ссылки, кому как удобнее). Очень легко создаются:

    ref var ptr = ref myObj.myField;

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

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

    Затем идет fixed -- в принципе логично, но почему-то не упомянут GCHandle.Alloc(..., Pinned), который тоже прибивает гвоздями целый объект пока вы не освободите полученный хэндл. И тоже можно достать указатель. Заплатив правда оверхэд за приколачивание памяти к указателю.

    С Marshal.Alloc* не все понятно. Было бы неплохо объяснить разницу и еще уточнить, что произойдет, если вызвать AllocCoTaskMem() на ОС отличной от Windows (где COM просто нет). Я например не знаю (но теперь видимо попробую, т.к. на MSDN про это не сказано).

    Ну и наконец, где же Pinned Object Heap? Штука ведь специльно для этого, доступна с .NET 5, целый год этой фиче уже. В случае POH fixed операция становится бесплатной. Вангую, что какой-нибудь Unsafe.AsPointer(ref pohArr[0]) будет тоже бесплатным.

    При этом стоит уточнить, что если речь идет о вызове каких-то нативных методов, то встроенный маршалер может справиться со многими задачами сам, если ему правильно накинуть атрибутов на импортируемую функцию. Могу ошибаться, но он кажется теперь даже со Span работает как надо. А если речь идет не о массивах, а о, например, структурах (типичный паттерн -- создаете структуру, отдаете ее методу, который заполняет в ней поля, после чего используете ее для конфигурации), то там вообще достаточно out/ref/in (возможно, с дополнительными атрибутами). И никаких указателей в том смысле, в котором вы о них тут рассуждаете, не надо.

    Поправьте/дополните, если что-то упустил/наврал.


    1. BkmzSpb
      16.08.2021 01:24
      +2

      Отвечу сам себе:

      Code
      
      using System;
      using System.Runtime.InteropServices;
      var ptr = IntPtr.Zero;
      try
      {
          Console.WriteLine("Allocating memory...");
          ptr = Marshal.AllocCoTaskMem(4097);
          Console.WriteLine($"Memory allocated at 0x{ptr.ToInt64().ToString("X16")}");
      }
      catch(Exception e1)
      {
          Console.WriteLine($"Failed to allocate memory: \"{e1.Message}\"");
      }
      finally 
      {
          if (ptr != IntPtr.Zero) 
          {
              Console.WriteLine("Freeing memory...");
              try 
              {
                 Marshal.FreeCoTaskMem(ptr);
              }
              catch(Exception e2)
              {
                  Console.WriteLine($"Failed to free memory >_< : \"{e2.Message}\"");
              }
          }
      }

      Далее делаем dotnet run:

      • Windows 10 x64

        • Allocating memory...

        • Memory allocated at 0x000001ABB8D3D350

        • Freeing memory...

      • Ubuntu 20.04 @ WSL2

        • Allocating memory...

        • Memory allocated at 0x0000557081DED9E0

        • Freeing memory...

      Может это фича WSL на Windows, но похоже работает.


    1. WhiteBlackGoose
      16.08.2021 07:05
      +4

      А еще не забываем про класс NativeMemory, который, как несложно догадаться, аллоцирует именно нативно (в частности, можем аллоцировать выровненную (aligned) память с помощью NativeMemory.AllocAligned)


      1. BkmzSpb
        16.08.2021 13:00
        +1

        Спасибо, по какой-то причине я это совсем упустил. Если я правильно понял по дате, замерджили это совсем недавно, и доступно это API будет не раньше .NET 6?


        1. WhiteBlackGoose
          16.08.2021 13:04
          +1

          Да, это фича начиная с шестого дотнета (возможно в седьмом превью уже есть, не помню)


    1. darkagent
      16.08.2021 09:50
      +1

      комментарий полезней статьи, спасибо за наводки на POH.


  1. forever_live
    16.08.2021 01:47
    +2

    Зря вы назвали первую функцию Malloc. При первом прочтении может сложиться впечатление, что это была объявлена специальная функция для выделения памяти, хотя по сигнатуре становится ясно, что это не так. Тем более, что указатель на объект, выделенный на стеке было бы бессмысленно и опасно возвращать из функции. Эта память будет освобождена после завершения Malloc() и переиспользована.


  1. gdt
    16.08.2021 05:32

    Это всё конечно очень круто, но мне почему-то всегда казалось, что для работы с unmanaged библиотеками нужно использовать P/Invoke, а не выделять руками память. Делал (и в общем-то продолжаю делать) managed врапперы для нескольких немаленьких unmanaged библиотек различного рода, как-то так сложилось что возможностей P/Invoke для этих целей было всегда более чем достаточно.


    1. a-tk
      16.08.2021 08:43
      +1

      Это ортогональные, то бишь независимые, вещи.

      Ситуации, когда требуются указатели, не всегда сводятся к вызову внешнего кода. Хотя сейчас C# и BCL развиваются в сторону сокрытия от разработчика действий в unsafe-контексте.


      1. gdt
        16.08.2021 08:50
        +1

        Я понимаю, это больше к мотивации в начале статьи:

        Указатели необходимы к примеру, для взаимодействия с библиотеками, использующими ручное выделение/удаление блоков памяти