This is the fourteenth part of the Custom memory allocation series. For your convenience you can find other parts in the table of contents in Part 1 — Allocating object on a stack
This whole series is about unsafe operations and manual memory managing. However, all the things I’ve shown can be done with no unsafe keyword at all and no TypedReference instances. Let’s start with getting an instance address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System; using System.Runtime.InteropServices; namespace Makeref_Safe { class Program { static void Main(string[] args) { int[] array = new int[20]; array[1] = (int)typeof(Klass).TypeHandle.Value; array[2] = 123; GCHandle arrayHandle = GCHandle.Alloc(array); var arrayAddress = Marshal.ReadIntPtr(GCHandle.ToIntPtr(arrayHandle)); Holder<Klass> holder = new Holder<Klass>(); GCHandle holderHandle = GCHandle.Alloc(holder); var holderAddress = Marshal.ReadIntPtr(GCHandle.ToIntPtr(holderHandle)); Marshal.WriteIntPtr(holderAddress, 4, arrayAddress + 3 * 4); // Skip Method Handle, array size, first integer for sync block, assumes x86 Console.WriteLine(holder.reference.GetType()); Console.WriteLine(holder.reference.Field); array[2] = 456; Console.WriteLine(holder.reference.Field); } } class Holder<T> { public T reference; } class Klass { public int Field; } } |
You can see the output:
1 2 3 |
Klass 123 456 |
So you can see that I’m able to get the object address and use it to modify anything in place. Whereas with TypedReference we could modify any reference directly, this time we need to wrap it with some holder to get another level of indirection. However, the concept is exactly the same.
What about implementing the UnsafeList in the safe way?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
using System.Runtime.InteropServices; namespace UnsafeList { public class SafeList<T> where T : class { private readonly int _elementSize; private T _target; private int[] _storage; private int _currentIndex = -1; private Holder<T> holder = new Holder<T>(); GCHandle storageHandle; GCHandle holderHandle; public SafeList(int size, int elementSize) { _elementSize = elementSize; _storage = new int[size * _elementSize]; _target = default(T); storageHandle = GCHandle.Alloc(_storage); holderHandle = GCHandle.Alloc(holder); } public int Add(T item) { _currentIndex++; GCHandle handle = GCHandle.Alloc(item); var itemAddress = Marshal.ReadIntPtr(GCHandle.ToIntPtr(handle)) - 4; // Assumes 86 handle.Free(); for (int i = 1; i < _elementSize; ++i) { _storage[_currentIndex*_elementSize + i] = Marshal.ReadInt32(itemAddress + i * 4); } return _currentIndex; } public T GetInternal(int index) { var storageAddress = Marshal.ReadIntPtr(GCHandle.ToIntPtr(storageHandle)); var holderAddress = Marshal.ReadIntPtr(GCHandle.ToIntPtr(holderHandle)); Marshal.WriteIntPtr(holderAddress, 4, storageAddress + 2 * 4 + index * _elementSize * 4 + 4); // Skip 2*4 for Method Handle and array size, index * _elementSize * 4 for elements, 4 for sync block return holder.reference; } public T Get(int index) { return GetInternal(index); } public void Free() { storageHandle.Free(); holderHandle.Free(); } } class Holder<T> { public T reference; } } |
Exactly the same principles. However, this time we can benchmark it in Dotnetfiddle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
------------------------ Array Insertion time: 8 Sum: -1610778624 Calculation time: 4 Array Insertion time: 20 Sum: -1610778624 Calculation time: 4 Array Insertion time: 32 Sum: -1610778624 Calculation time: 3 ------------------------ List Insertion time: 7 Sum: -1610778624 Calculation time: 3 List Insertion time: 16 Sum: -1610778624 Calculation time: 3 List Insertion time: 42 Sum: -1610778624 Calculation time: 3 ------------------------ SafeList Insertion time: 55 Sum: -1610778624 Calculation time: 12 SafeList Insertion time: 54 Sum: -1610778624 Calculation time: 10 SafeList Insertion time: 68 Sum: -1610778624 Calculation time: 10 |
We know from previous parts that UnsafeList was faster with huge number of elements. Here we have only 100k and we can see the UnsafeList implemented in a “safe way” is way slower. My output for 20kk elements:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
------------------------ Array Insertion time: 3412 Sum: -1023623168 Calculation time: 109 Array Insertion time: 3437 Sum: -1023623168 Calculation time: 114 Array Insertion time: 3441 Sum: -1023623168 Calculation time: 108 ------------------------ List Insertion time: 3310 Sum: -1023623168 Calculation time: 148 List Insertion time: 3563 Sum: -1023623168 Calculation time: 118 List Insertion time: 3413 Sum: -1023623168 Calculation time: 148 ------------------------ UnsafeList Insertion time: 1184 Sum: -1023623168 Calculation time: 174 UnsafeList Insertion time: 1304 Sum: -1023623168 Calculation time: 210 UnsafeList Insertion time: 1278 Sum: -1023623168 Calculation time: 177 ------------------------ SafeList Insertion time: 6714 Sum: -1023623168 Calculation time: 573 SafeList Insertion time: 7168 Sum: -1023623168 Calculation time: 584 SafeList Insertion time: 6746 Sum: -1023623168 Calculation time: 593 |
So you can see that the UnsafeList is faster (~1 second versus ~3.5). SafeList on the other hand is much slower (almost 7 seconds). However, no unsafe keyword.