Создаём массив своими руками в 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(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/


Интересные статьи

Интересные статьи

Всем привет, меня зовут Алексей Остриков, я руковожу разработкой в Яндекс.Маркете. Когда-то я много-много писал код, затем полтора года руководил группой бэкенда одного из сервисов Маркет...
А давайте напишем своего крутого бота-модератора чатов на Python. Пусть он сможет чистить чат, банить участников и выдавать им предупреждения, приветствовать новых участн...
В этой статье мы расскажем, как оптимизировать крупный проект в «Битрикс24» и увеличить его производительность в 3 раза, изменяя настройки MySQL и режим питания CPU. Дано Корпоративн...
Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...
Представляю вашему вниманию перевод статьи «Создаем музыку: когда простые решения превосходят по эффективности глубокое обучение» о том, как искусственный интеллект применяется для создания музык...