Доброго времени суток!
Как известно, в CLR у каждого массива есть заголовок — два IntPtr, один из которых указывает на тип элемента массива, а другой описывает его размер.
Логично предположить, что если изменить тип элементов массива, скажем с byte на int, а размер массива, соответственно, уменьшить в 4 раза, при условии, что он кратен 4, упаковать его в (object) и распаковать, как (int[]), мы получим массив нового типа без какого-либо копирования элементов. Быстро и удобно.
В большинстве случаев это не требуется, и можно обойтись unsafe/fixed и кастом типа указателей.
Но далеко не все методы .NET умеют работать с указателями. Наглядный пример — методы Read/Write потоков.
Но есть проблема — работает вышеописанный способ лишь до первой сборки мусора. Как только GC пытается удалить осиротевший массив, претерпевший подобное изнасилование, приложение падает.
Вопрос — чего не хватает? Как научить GC работать со старыми массивами, у которых изменился тип?
| Текущая реализация грязного хака |
| int[] result = _input.DungerousReadStructs<Int32>(entry.Length / 4);
public static T[] DungerousReadStructs<T>(this Stream input, int count) where T : struct
{
if (count < 1)
return new T[0];
Array result = new T[count];
Int32 entrySize = UnsafeTypeCache<T>.UnsafeSize;
using (UnsafeTypeCache<byte>.ChangeArrayType(result, entrySize))
input.EnsureRead((byte[])result, 0, result.Length);
return (T[])result;
}
public static class TypeCache<T>
{
public static readonly Type Type = typeof(T);
}
public static class UnsafeTypeCache<T>
{
public static readonly Int32 UnsafeSize = GetSize();
public static readonly UIntPtr ArrayTypePointer = GetArrayTypePointer();
private static Int32 GetSize()
{
DynamicMethod dynamicMethod = new DynamicMethod("SizeOf", typeof(Int32), Type.EmptyTypes);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Sizeof, TypeCache<T>.Type);
generator.Emit(OpCodes.Ret);
return ((Func<int>)dynamicMethod.CreateDelegate(typeof(Func<Int32>)))();
}
private static unsafe UIntPtr GetArrayTypePointer()
{
T[] result = new T[1];
using (SafeGCHandle handle = new SafeGCHandle(result, GCHandleType.Pinned))
return *(((UIntPtr*)handle.AddrOfPinnedObject().ToPointer()) - 2);
}
public static IDisposable ChangeArrayType(Array array, Int32 oldElementSize)
{
if (array.Length < 1)
throw new NotSupportedException();
SafeGCHandle handle = new SafeGCHandle(array, GCHandleType.Pinned);
try
{
unsafe
{
UIntPtr* arrayPointer = (UIntPtr*)handle.AddrOfPinnedObject().ToPointer();
UIntPtr arrayLength = *(arrayPointer - 1);
UIntPtr arrayType = *(arrayPointer - 2);
UInt64 arraySize = ((UInt64)arrayLength * (UInt64)oldElementSize);
if (arraySize % (UInt64)UnsafeSize != 0)
throw new InvalidCastException();
try
{
*(arrayPointer - 1) = new UIntPtr(arraySize / (UInt64)UnsafeSize);
*(arrayPointer - 2) = ArrayTypePointer;
return new DisposableAction(() =>
{
*(arrayPointer - 1) = arrayLength;
*(arrayPointer - 2) = arrayType;
handle.Dispose();
});
}
catch
{
*(arrayPointer - 1) = arrayLength;
*(arrayPointer - 2) = arrayType;
throw;
}
}
}
catch
{
handle.SafeDispose();
throw;
}
}
}
Работает. А вот если перед тем, как я дёрну "Free" не вернуть значения типа и длины к оригинальным, сборка мусора крашет приложение.
Вот и возник вопрос — отчего так? Помимо заголовка архива, тип объекта хранится где-то ещё? (н.п. в объявлении локальной переменной?). Можно ли и его изменить?
Также интересует — как в такой случае правильно описывать структуры? Marshaling работать не будет — это понятно. Соответственно, никаких ссылочных типов.
Вопрос: Как прикрутить собственный маршалинг, н.п. для корректного распознавания строк в той или иной кодировке? Вложенных массивов (аналог ByValStr, ByValArray)
Вопрос: Нужно ли структурам задавать аттрбиут StructLayout и будет ли он использоваться (или это часть механизма маршалинга?)
Вопрос: К чему приведёт использование подобного хака в совокупности со stackalloc? |
| |
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт