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



Этот пример показывает, что фактически мы работаем с хранилищем внутри экземпляра класса ArrayNoGarbage. Но делаем это через полноценный массив, который существует как отдельная сущность.
Реализация
Подготовка. Нужно разрешить unsafe контекст. И установить через NuGet пакет System.Runtime.CompilerServices.Unsafe если его ещё нет в проекте. Класс Unsafe содержит универсальные низкоуровневые функции для управления управляемыми и неуправляемыми указателями.
План действий. Выделить участок памяти (хранилище), в котором будет расположен массив и служебные данные. При запросе на изменение длины массива, будем вручную менять его длину. Создадим массив с null значением и подсунем ему наш участок памяти.

Создаём 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/
Добавить комментарий