Создаём массив своими руками в c#

от автора

Сейчас я покажу как можно создать массив, у которого можно менять размер, не используя для этого нативные возможности языка. Т.е. вместо Array.Resize и var array = new int[100] только прямой доступ к памяти через unsafe контекст, который поможет избежать копирования и выделения новой памяти.

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

Сначала начну с результатов. Чтобы лучше было понятно, ради чего всё это.

Вот так выглядит хранилище после первого цикла с заполнением данными.
Вот так выглядит хранилище после первого цикла с заполнением данными.
И вот что получилось после второго цикла.
И вот что получилось после второго цикла.

Этот пример показывает, что фактически мы работаем с хранилищем внутри экземпляра класса ArrayNoGarbage. Но делаем это через полноценный массив, который существует как отдельная сущность.

Реализация

Подготовка. Нужно разрешить unsafe контекст. И установить через NuGet пакет System.Runtime.CompilerServices.Unsafe если его ещё нет в проекте. Класс Unsafe содержит универсальные низкоуровневые функции для управления управляемыми и неуправляемыми указателями.

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

Устройство массива в памяти. Иллюстрация с сайта microsoft.
Устройство массива в памяти. Иллюстрация с сайта microsoft.

Создаём generic класс ArrayNoGarbage, конструктор будет принимать значение, указывающее ёмкость. В дальнейшем эту ёмкость будем увеличивать если она меньше запрашиваемой длины.

В качестве generic типов будем принимать только unmanaged типы.

Хранилищем будет generic массив. Это поможет упростить код чтобы избавить себя от ручного менеджмента через класс Marshal. А также поможет избежать ручного объявления служебных данных, таких как Object Header и Method Table Ptr. Часть этого хранилища будет зарезервирована под служебные данные для нашего динамического массива. Под хранилище будет выделяться память один раз в конструкторе. Либо при создании массива если запрашиваемая длина больше, чем ёмкость хранилища. Кроме этого, никаких выделений памяти не будет.

Ниже полный код класса ArrayNoGarbage с подробными комментариями.

using System; using System.Runtime.CompilerServices;  //Компилятор ругается на то, что массив является null поэтому подавляем эти предупреждения. #pragma warning disable CS8618 #pragma warning disable CS8601 public sealed unsafe class ArrayNoGarbage<T> where T : unmanaged {     //Хранилище для служебных и пользовательских данных.     private T[] storage;          //Этот массив будет создаваться вручную и являться частью хранилища данных.     //Фактически он не занимает места в памяти.     private T[] array;          //Количество элементов хранилища выделенное под служебные данные.     private readonly int elementOffset;          //Размер служебных данных таких как Object Header, Method Table Ptr и длина массива.     private readonly int ptrOffset;          //Минимальная вместимость хранилища.     private const int MIN_CAPACITY = 64;      public ArrayNoGarbage(int capacity = MIN_CAPACITY)     {         //Инициализация хранилища с заданной ёмкостью. Хранилище – не массив, который мы будем создавать вручную.          storage = new T[capacity < MIN_CAPACITY ? MIN_CAPACITY : capacity];                  //Ссылка на хранилище.         var storageObjPtr = (IntPtr*) Unsafe.AsPointer(ref storage);         //Ссылка на заголовк хранилища.         var storageHeaderPtr = (*storageObjPtr).ToPointer();         //Ссылка на первый элемент хранилища.         var storageElement0Ptr = Unsafe.AsPointer(ref storage[0]);          //Находим размер служебных данных.         ptrOffset = (int) ((((IntPtr) storageElement0Ptr).ToInt64() -                             ((IntPtr) storageHeaderPtr).ToInt64()) / sizeof(nint));         //Вычисляем в какое количество элементов хранилища поместятся служебные данные.         elementOffset = sizeof(nint) * ptrOffset <= sizeof(T)             ? 1             : sizeof(nint) * ptrOffset / sizeof(T);                  //Ссылка на первый элемент создаваемого массива.         //Unsafe.Add и Unsafe.Subtract вычисляют смещение в памяти относительно текущего.         var arrayElement0Ptr = Unsafe.Add<T>(storageElement0Ptr, elementOffset);         //Ссылка на заголовок массива.         var arrayHeaderPtr = (nint*) Unsafe.Subtract<nint>(arrayElement0Ptr, ptrOffset);          //Копируем служебные данные массива,         //на данном этапе они полностью совпадают с данными хранилища,         //а потом начинают жить своей жизнью.         for (var i = 0; i < ptrOffset; i++)         {             arrayHeaderPtr[i] = ((nint*) storageHeaderPtr)[i];         }          //Ссылка на участок памяти в котором хранится длина массива.         var lengthPtr = (int*) Unsafe.Subtract<nint>(arrayElement0Ptr, 1);         //Устаналиваем длину массива за вычетом служебных данных.         lengthPtr[0] = storage.Length - elementOffset;                  //Ссылка на массив. Сейчас он равен null. И ссылается на IntPtr.Zero.         var arrayObjPtr = (nint*) Unsafe.AsPointer(ref array);         //Записываем новую ссылку на заголовок массива. Теперь массив больше не null.         arrayObjPtr[0] = (nint) arrayHeaderPtr;     }      public T[] GetArray(int length)     {         //Если запрашиваемая длина совпадает с длиной массива, то ничего не делаем.         if (length == array.Length) return array;                  //Если запрашиваемая длина больше, чем емкость выделенная под пользовательские данные,         //то изменяем размер хранилища.         if (length > storage.Length - elementOffset)         {             //Array.Resize используется только для увеличения ёмкости хранилища, когда запрашиваемая длина превышает доступную ёмкость.             //Эта часть кода не имеет никакого отношения к манипуляциям с памятью для создания массива в существующем хранилище.             Array.Resize(ref storage, length + elementOffset);                          //Ссылка на первый элемент хранилища.             var storageElement0Ptr = Unsafe.AsPointer(ref storage[0]);             //Ссылка на первый элемент массива.             var arrayElement0Ptr = Unsafe.Add<T>(storageElement0Ptr, elementOffset);             //Ссылка на заголовок массива.             var arrayHeaderPtr = Unsafe.Subtract<nint>(arrayElement0Ptr, ptrOffset);             //Ссылка на массив.             var arrayObjPtr = (nint*) Unsafe.AsPointer(ref array);              //Размер хранилища был изменён,             //наш массив сейчас ссылается на старый участок памяти.             //Записываем новую ссылку на заголовок массива.             //Старый участок памяти будет очищен сборщиком мусора             //на него больше не ссылается ни хранилище, ни наш массив.             arrayObjPtr[0] = (nint) arrayHeaderPtr;         }                  {             //Ссылка на первый элемент массива.             var arrayElement0Ptr = Unsafe.AsPointer(ref array[0]);             //Ссылка на участок памяти в котором хранится длина массива.             var lengthPtr = (int*) Unsafe.Subtract<IntPtr>(arrayElement0Ptr, 1);             //Устанавливаем новую длину массива.             lengthPtr[0] = length;         }                  return array;     } } #pragma warning restore CS8618 #pragma warning restore CS8601

Я не проводил тестирования в 32 битной среде. Возможно, есть какие-то проблемы.

Заключение

На этом всё. С помощью нехитрых манипуляций с памятью мы создали свой массив внутри другого массива и полностью управляем его размером без нагрузки на сборщик мусора.


ссылка на оригинал статьи https://habr.com/ru/post/715020/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *