📜 ⬆️ ⬇️

Continuing to shred CLR: .Net object pool outside SOH / LOH heaps

Good afternoon, dear developers (just did not know where to start the post). I suggest that before the work week begins a little podramzyat brains (quite a bit) and build your Small Objects Heap for .Net. Or rather not even the Small Objects Heap, but the Custom Objects Heap.

As we all know, in .Net there are two groups of heaps: for large and small objects. How to find out how much an object will cost us using the code from this article (it will be useful to us): Manual cloning of the stream , and getting a pointer to an object and using the pointer to get the object itself can be learned by reading this article: Getting a pointer to an .Net object . We also need an article from a Korean (southern) programmer to redirect the pointer to the compiled part of the method to another method:: CL 에 메서드 가로 채기 - CLR Injection: Runtime Method Replacer

So let's experiment and write a library that will allow:

')
Link to the project on GitHub: DotNetEx


Those. Let's write a pool of objects out of .Net memory.
We will solve problems as questions arise.


Are you ready? Let's get started.

The first thing we define is types

internal static class Stub { public static void Construct(object obj, int value) { } } 

This is a type whose only static method will be changed so that the pointer to the compiled part of the method will look at the constructor of our type. The first parameter, obj , is essentially a this pointer. After all, as you know, this is nothing but the first parameter of the method, which is always in each instance method. The second parameter is an integer. Introduced to verify that we can transmit integers.

 public class UnmanagedObject<T> : IDisposable where T : UnmanagedObject<T> { internal IUnmanagedHeap<T> heap; #region IDisposable implementation void IDisposable.Dispose() { heap.Free(this); } #endregion } 

Next, we introduce the UnmanagedObject type to firstly introduce a method of returning an object to the Dispose () pool, and secondly, to architecturally separate all objects intended to be placed outside the CLR of heaps from the standard ones. The only field of the class is of type internal, so that it can be set from the outside, in the pool of objects.

And the last - the class of the pool itself.

 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); } } 


In order:
In the class constructor, we first calculate the size of the type instance. After that, multiply by capacity, add the size of tables of free / occupied slots and get the size of the pool. Having received the size, we allocate a pool in virtual memory. Then we get the descriptors of the methods: the type constructor and the stub and at the stub we set a pointer to the method body as the constructor body:
  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); 

The last is in a loop, for each future object we put a pointer to the table of Wirth methods, we do the casting in .Net type and set the heap field of the newly initialized object in our pool.

Of particular interest is the Allocate method:

  public TPoolItem Allocate() { _freeSize--; var obj = _freeObjects[_freeSize]; Stub.Construct(obj, 123); return EntityPtr.ToInstanceWithOffset<TPoolItem>(obj); } 

In it, we first take the last of them from the table of free objects. Then we call the Construct method of the Stub class, the body of which is in fact our constructor for the class of the pool element. The constructor is passed 123 as a parameter.

Using


Use test using the following code
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(); } } } 


Output to console:

Source: https://habr.com/ru/post/247433/


All Articles