Добрый день, уважаемые разработчики (просто не знал, с чего начать пост). Предлагаю перед тем как начнется трудовая неделя немного подразмять мозги (совсем немного) и построить свой Small Objects Heap для .Net. Вернее даже не Small Objects Heap, а Custom Objects Heap.
Как все мы знаем, в .Net существует две группы куч: для больших и малых объектов. Как выяснить, во сколько нам обойдется объект можно при помощи кода из этой статьи (он нам пригодится):
Ручное клонирование потока, а получить указатель на объект и по указателю получить сам объект можно научиться, прочтя эту статью:
Получение указателя на объект .Net. Также нам понадобится статья корейского (южно-) программиста по перенаправлению указателя на скомпилированную часть метода на другой метод: 실행 시에 메서드 가로채기 — CLR Injection: Runtime Method Replacer 개선
Так что давайте поэкспериментируем и напишем библиотеку, которая позволит:
Ссылка на проект на GitHub:
DotNetEx
Т.е. напишем пул объектов вне .Net памяти.
Будем решать задачи по мере поступления вопросов.
Первое, что мы определяем – типы
Это — тип, единственный статический метод которого будет изменен так, что указатель на скомпилированную часть метода будет смотреть на конструктор нашего типа. Первый параметр — obj — является по своей сути указателем this. Ведь, как вы знаете, this есть ни что иное как первый параметр метода, который есть всегда в каждом экземплярном методе. Второй параметр — целое число. Введен для того чтобы проверить что мы можем передавать целые числа.
Далее введем тип UnmanagedObject чтобы во-первых ввести метод возврата объекта в пул Dispose(), а во-вторых архитектурно отделить все объекты, предназначенные для размещения вне CLR куч от стандартных. Единственное поле класса типа internal, чтобы его можно было задать извне, в пуле объектов.
И последнее — класс самого пула.
По порядку:
В конструкторе класса мы сначала рассчитываем размер экземпляра типа. После чего умножаем на capacity, добавляем размер таблиц свободных/занятых слотов и получаем размер пула. Получив размер, аллоцируем пул в виртуальной памяти. После чего получаем описатели методов: конструктора типа и заглушки и у заглушки выставляем указатель на тело метода как тело конструктора:
Последнее — в цикле, у каждого будущего объекта проставляем указатель на таблицу вирт методов, делаем кастинг в .Net тип и выставляем поле heap у только что проинициализированного объекта в наш пул.
Отдельный интерес представляет метод Allocate:
В нем мы сначала из таблицы свободных объектов берем последний из них. После чего вызываем метод Construct класса Stub, тело которого на самом деле — наш конструктор класса элемента пула. Конструктору передаем число 123 как параметр.
Использование протестируем с помощью следующего кода
using System;
using System.Runtime.CLR;
Вывод в консоль:

Как все мы знаем, в .Net существует две группы куч: для больших и малых объектов. Как выяснить, во сколько нам обойдется объект можно при помощи кода из этой статьи (он нам пригодится):
Ручное клонирование потока, а получить указатель на объект и по указателю получить сам объект можно научиться, прочтя эту статью:
Получение указателя на объект .Net. Также нам понадобится статья корейского (южно-) программиста по перенаправлению указателя на скомпилированную часть метода на другой метод: 실행 시에 메서드 가로채기 — CLR Injection: Runtime Method Replacer 개선Так что давайте поэкспериментируем и напишем библиотеку, которая позволит:
- Аллоцировать участок памяти
- Разметить его как набор объектов определенного .Net типа
- Выделять объекты с этой памяти
- Возвращать их обратно
Ссылка на проект на GitHub:
DotNetExДля начала, полный список выложенных на Хабре статей данного цикла
Делаем отгружаемые сборки: взаимодействуем между доменами без маршаллинга
Получение указателя на объект .Net
Ручное клонирование потока. Когда Assembler + C# или Java = Love
Изменение кода системных сборок или «утечка» .Net Framework 5.0
Как работает декомпиляция в .Net или Java на примере .Net
Продолжаем кромсать CLR: пул объектов .Net вне куч SOH/LOH
Снимаем дамп объектов с памяти .Net приложения
Получение указателя на объект .Net
Ручное клонирование потока. Когда Assembler + C# или Java = Love
Изменение кода системных сборок или «утечка» .Net Framework 5.0
Как работает декомпиляция в .Net или Java на примере .Net
Продолжаем кромсать CLR: пул объектов .Net вне куч SOH/LOH
Снимаем дамп объектов с памяти .Net приложения
Т.е. напишем пул объектов вне .Net памяти.
Будем решать задачи по мере поступления вопросов.
- Как разметить уже саллоцированный участок памяти каким-либо объектом?
В любом объектно-ориентированном языке объект состоит из указателя на таблицу виртуальных методов и полей объекта. Таблица эта необходима чтобы понимать какие из перегрузок методов должны быть вызваны и сама по себе нам не так интересна в рамках данной задачи. Если мы работаем с каким-либо объектом и вызываем «у него» метод, это значит что сначала будет загружен адрес объекта, по нему будем расположен адрес таблицы виртуальных методов. А по этому адресу мы берем указатель на нужный метод и вызываем его. Значит чтобы любой участок памяти представить как объект определенного типа необходимо просто записать указатель на таблицу виртуальных методов.
Как мы это можем сделать? Сначала я брал экземпляр существующего объекта, и вычитывал первые 4 или 8 байт, записывая их к себе. Метод работал, но он не красивый. Нужен экземпляр. После чего я нашел что этот адрес легко вычитывается с помощью свойства typeof(TType).TypeHandle. - Как выделить кусок памяти?
Это сделать совсем просто: есть функцияMarshal.AllocHGlobal(_totalSize), которая позволяет выделить любое требуемое количество виртуальной памяти. Если вам при этом надо разместить эту память по любому адресу, то надо воспользоваться ее WinApi аналогом. - А как же вызов конструктора?
Для того чтобы вызвать конструктор, у нас три пути:
- Сделать метод Init и вызывать его. Это не очень красиво, не очень спортивно. Однако, не надо лезть в рефлексию и во внутренности .Net CLR.
- Вызывать конструктор через рефлексию. Более спортивный метод, однако рефлексия налагает определенные тормоза.
- После редактирования таблицы скомпилированных тел методов вызвать другой метод, но при этом будет вызван конструктор. Это мозговыносящий метод и в нем – наибольший процент спорта =) Им и воспользуемся. Ведь получится прямой вызов, без посредников. Как будто конструктор и вызвали.
Ну что, готовы? Давайте приступим.
Первое, что мы определяем – типы
internal static class Stub { public static void Construct(object obj, int value) { } }
Это — тип, единственный статический метод которого будет изменен так, что указатель на скомпилированную часть метода будет смотреть на конструктор нашего типа. Первый параметр — obj — является по своей сути указателем this. Ведь, как вы знаете, this есть ни что иное как первый параметр метода, который есть всегда в каждом экземплярном методе. Второй параметр — целое число. Введен для того чтобы проверить что мы можем передавать целые числа.
public class UnmanagedObject<T> : IDisposable where T : UnmanagedObject<T> { internal IUnmanagedHeap<T> heap; #region IDisposable implementation void IDisposable.Dispose() { heap.Free(this); } #endregion }
Далее введем тип UnmanagedObject чтобы во-первых ввести метод возврата объекта в пул Dispose(), а во-вторых архитектурно отделить все объекты, предназначенные для размещения вне CLR куч от стандартных. Единственное поле класса типа internal, чтобы его можно было задать извне, в пуле объектов.
И последнее — класс самого пула.
public unsafe class UnmanagedHeap<TPoolItem> : IUnmanagedHeap<TPoolItem> where TPoolItem : UnmanagedObject<TPoolItem> { private readonly IntPtr *_freeObjects; private readonly IntPtr *_allObjects; private readonly int _totalSize, _capacity; private int _freeSize; private readonly void *_startingPointer; private readonly ConstructorInfo _ctor; public UnmanagedHeap(int capacity) { _freeSize = capacity; // Getting type size and total pool size var objectSize = GCEx.SizeOf<TPoolItem>(); _capacity = capacity; _totalSize = objectSize * capacity + capacity * IntPtr.Size * 2; _startingPointer = Marshal.AllocHGlobal(_totalSize).ToPointer(); var mTable = (MethodTableInfo*)typeof(TPoolItem).TypeHandle.Value.ToInt32(); _freeObjects = (IntPtr*)_startingPointer; _allObjects = (IntPtr*)((long)_startingPointer + IntPtr.Size * capacity); _startingPointer = (void*)((long)_startingPointer + 2 * IntPtr.Size * capacity); var pFake = typeof(Stub).GetMethod("Construct", BindingFlags.Static|BindingFlags.Public); var pCtor = _ctor = typeof(TPoolItem).GetConstructor(new []{typeof(int)}); MethodUtil.ReplaceMethod(pCtor, pFake, skip: true); for(int i = 0; i < capacity; i++) { var handler = (IntPtr *)((long)_startingPointer + (objectSize * i)); handler[1] = (IntPtr)mTable; var obj = EntityPtr.ToInstance<object>((IntPtr)handler); var reference = (TPoolItem)obj; reference.heap = this; _allObjects[i] = (IntPtr)(handler + 1); } Reset(); } public int TotalSize { get { return _totalSize; } } public TPoolItem Allocate() { _freeSize--; var obj = _freeObjects[_freeSize]; Stub.Construct(obj, 123); return EntityPtr.ToInstanceWithOffset<TPoolItem>(obj); } public void Free(TPoolItem obj) { _freeObjects[_freeSize] = EntityPtr.ToPointerWithOffset(obj); _freeSize++; } public void Reset() { WinApi.memcpy((IntPtr)_freeObjects, (IntPtr)_allObjects, _capacity * IntPtr.Size); _freeSize = _capacity; } object IUnmanagedHeapBase.Allocate() { return this.Allocate(); } void IUnmanagedHeapBase.Free(object obj) { this.Free((TPoolItem)obj); } public void Dispose() { Marshal.FreeHGlobal((IntPtr)_freeObjects); } }
По порядку:
В конструкторе класса мы сначала рассчитываем размер экземпляра типа. После чего умножаем на capacity, добавляем размер таблиц свободных/занятых слотов и получаем размер пула. Получив размер, аллоцируем пул в виртуальной памяти. После чего получаем описатели методов: конструктора типа и заглушки и у заглушки выставляем указатель на тело метода как тело конструктора:
var pFake = typeof(Stub).GetMethod("Construct", BindingFlags.Static|BindingFlags.Public); var pCtor = _ctor = typeof(TPoolItem).GetConstructor(new []{typeof(int)}); MethodUtil.ReplaceMethod(pCtor, pFake, skip: true);
Последнее — в цикле, у каждого будущего объекта проставляем указатель на таблицу вирт методов, делаем кастинг в .Net тип и выставляем поле heap у только что проинициализированного объекта в наш пул.
Отдельный интерес представляет метод Allocate:
public TPoolItem Allocate() { _freeSize--; var obj = _freeObjects[_freeSize]; Stub.Construct(obj, 123); return EntityPtr.ToInstanceWithOffset<TPoolItem>(obj); }
В нем мы сначала из таблицы свободных объектов берем последний из них. После чего вызываем метод Construct класса Stub, тело которого на самом деле — наш конструктор класса элемента пула. Конструктору передаем число 123 как параметр.
Использование
Использование протестируем с помощью следующего кода
using System;
using System.Runtime.CLR;
namespace UnmanagedPoolSample { class Program { /// <summary> /// Now cannot call default .ctor /// </summary> private class Quote : UnmanagedObject<Quote> { public Quote(int urr) { Console.WriteLine("Hello from object .ctor"); } public int GetCurrent() { return 100; } } static void Main(string[] args) { using (var pool = new UnmanagedHeap<Quote>(1000)) { using (var quote = pool.Allocate()) { Console.WriteLine("quote: {0}", quote.GetCurrent()); } } Console.ReadKey(); } } }
Вывод в консоль:

