Как известно, в 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?
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
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();
GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForFullGCComplete();
try
{
*(arrayPointer - 1) = new UIntPtr(arraySize / (UInt64)UnsafeSize);
*(arrayPointer - 2) = ArrayTypePointer;
GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForFullGCComplete();
return new DisposableAction(() =>
{
*(arrayPointer - 1) = arrayLength;
*(arrayPointer - 2) = arrayType;
handle.Dispose();
GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForFullGCComplete();
});
}
catch
{
*(arrayPointer - 1) = arrayLength;
*(arrayPointer - 2) = arrayType;
throw;
}
}
}
catch
{
handle.SafeDispose();
throw;
}
}
}
Работает. А вот если перед тем, как я дёрну "Free" не вернуть значения типа и длины к оригинальным, сборка мусора крашет приложение.
Вот и возник вопрос — отчего так? Помимо заголовка архива, тип объекта хранится где-то ещё? (н.п. в объявлении локальной переменной?). Можно ли и его изменить?
Также интересует — как в такой случае правильно описывать структуры? Marshaling работать не будет — это понятно. Соответственно, никаких ссылочных типов.
Вопрос: Как прикрутить собственный маршалинг, н.п. для корректного распознавания строк в той или иной кодировке? Вложенных массивов (аналог ByValStr, ByValArray)
Вопрос: Нужно ли структурам задавать аттрбиут StructLayout и будет ли он использоваться (или это часть механизма маршалинга?)
Вопрос: К чему приведёт использование подобного хака в совокупности со stackalloc?
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[3]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Albeoris, Вы писали:
A>Работает. А вот если перед тем, как я дёрну "Free" не вернуть значения типа и длины к оригинальным, сборка мусора крашет приложение. A>Вот и возник вопрос — отчего так? Помимо заголовка архива, тип объекта хранится где-то ещё? (н.п. в объявлении локальной переменной?). Можно ли и его изменить?
Когда вызывается сборка мусора — начинается дефрагментация памяти. При этом ищутся все переменные и указатели как в стэке, так и в регистрах процессора. Найдя каждую из этих переменных, GC переносит область памяти и меняет значение. Но когда ты работаешь с указателем — GC не имеет возможности его отследить.
A>Также интересует — как в такой случае правильно описывать структуры? Marshaling работать не будет — это понятно. Соответственно, никаких ссылочных типов. A>Вопрос: Как прикрутить собственный маршалинг, н.п. для корректного распознавания строк в той или иной кодировке? Вложенных массивов (аналог ByValStr, ByValArray) A>Вопрос: Нужно ли структурам задавать аттрбиут StructLayout и будет ли он использоваться (или это часть механизма маршалинга?) A>Вопрос: К чему приведёт использование подобного хака в совокупности со stackalloc?
Непонятно что ты хочешь добиться. Если это чистый нет — то binaryserializer и вперед. Если это текстовой файл, то при чтении есть енкодеры на любой вкус. Если это файл с сишной структурой — то можно сериализовать/десериализовать через Marshal.Copy/вместе с StructLayout. А лучше BitConverter(чаще всего). Но никогда не пользоваться указателями на managed память.
Мне никогда не приходилось работать с указателями на managed память. Нужно иметь очень веские причины для этого. А лучше написать часть кода на С++.
Re[4]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, GlebZ, Вы писали:
GZ>Непонятно что ты хочешь добиться. Если это чистый нет — то binaryserializer и вперед. Если это текстовой файл, то при чтении есть енкодеры на любой вкус. Если это файл с сишной структурой — то можно сериализовать/десериализовать через Marshal.Copy/вместе с StructLayout. А лучше BitConverter(чаще всего). Но никогда не пользоваться указателями на managed память. GZ>Мне никогда не приходилось работать с указателями на managed память. Нужно иметь очень веские причины для этого. А лучше написать часть кода на С++.
Всё очень просто: для того чтобы считать массив Int32, нужно считать массив байт и декодировать его в Int32 (как это, например, делает BinaryReader при помощи BitConverter'а).
Если нужно считать структуру, нужно считать массив байт, после чего отмарашалить его при помощи метода PtrToStructure.
В обоих случая создаются два массива. В обоих случаях происходит копирование данных между ними. Когда речь идёт о 100 объектов, это не играет роли, но когда их миллионы, подобные телодвижения негативным образом сказываются на производительности.
Вместо этого, я создаю массив структур (для простоты, возьмём Int32). Пришпиливаю его и меняю в памяти тип, заявляя, что массив на самом деле байтовый. При помощи упаковки подсовываю его в метод Stream.Read, который наполняет предоставленную область памяти байтами, после чего я меняю тип массива на первоначальный. Таким образом получая массив нужных мне структур без лишнего копирования и двойного выделения памяти.
Написать для этого CLI/C++ сборку можно, но мне точно также придётся передавать в методы указатели на управляемую память, а код будет делать ровно тоже самое. Зачем, если это можно сделать в C#?
Тогда уж правильнее было бы сразу писать на С++. Но пока авторы стандарта не откажутся от поддержки совместимости с Си, ноги моей в нём не будет.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[5]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Albeoris, Вы писали:
A>Всё очень просто: для того чтобы считать массив Int32, нужно считать массив байт и декодировать его в Int32 (как это, например, делает BinaryReader при помощи BitConverter'а). A>Если нужно считать структуру, нужно считать массив байт, после чего отмарашалить его при помощи метода PtrToStructure. A>В обоих случая создаются два массива. В обоих случаях происходит копирование данных между ними. Когда речь идёт о 100 объектов, это не играет роли, но когда их миллионы, подобные телодвижения негативным образом сказываются на производительности.
Обычно такие негативы совершенно незаметны на фоне операций ввода/вывода на диск или сеть.
A>Вместо этого, я создаю массив структур (для простоты, возьмём Int32). Пришпиливаю его и меняю в памяти тип, заявляя, что массив на самом деле байтовый. При помощи упаковки подсовываю его в метод Stream.Read, который наполняет предоставленную область памяти байтами, после чего я меняю тип массива на первоначальный. Таким образом получая массив нужных мне структур без лишнего копирования и двойного выделения памяти.
Либо надежность и простота, либо быстрота и колдовство. Для первого случае и существует NetFramework.
Обычно сначала пишется программа а затем налаживается быстродействие с помощью профайлера, а не наоборот. Ошибки ты получишь точно, а вот профит вряд ли. Исключение только, алгоритмическая оптимизация (когда заранее ясна какая-то NP сложность)
Re[6]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, GlebZ, Вы писали:
GZ>Обычно такие негативы совершенно незаметны на фоне операций ввода/вывода на диск или сеть.
Безусловно. В моём случае, это не так. Нужно уложиться в 300 мсек. Оптимизирую всё, что можно.
GZ>Либо надежность и простота, либо быстрота и колдовство. Для первого случае и существует NetFramework.
С этим не соглашусь, так как недавно MS сами говорили, что одной из проблем .NET с которыми они будут бороться, является постоянное копирование памяти, что не позволяет C# тягаться в производительности с тем Си.
Безусловно, такие шутки не стоит делать в продакшен-коде, но почему бы не собрать подводные камни в домашнем проекте?
GZ>Обычно сначала пишется программа а затем налаживается быстродействие с помощью профайлера, а не наоборот. Ошибки ты получишь точно, а вот профит вряд ли. Исключение только, алгоритмическая оптимизация (когда заранее ясна какая-то NP сложность)
Профит получил — выиграл 450 мсек. Это очень много, когда речь идёт о пользовательском интерфейсе, который должен максимально быстро и без видимых задержек реагировать на действия пользователя.
На это все давным давно плюнули и по любому поводу втыкают "крутилки" ожидания. Опять же — в домашнем проекте. Ни в коем случае не призываю использовать данный подход в продакшен.
...да и вообще ни к чему не призываю: просто интересуюсь, чем это может грозить, и как ещё поизощрённее выстрелить себе в ногу, чтобы испытать чувство полного удовлетворения от создания того, что удалось реализовать ещё один грязный хак.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[5]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Albeoris, Вы писали:
A>Всё очень просто: для того чтобы считать массив Int32, нужно считать массив байт и декодировать его в Int32 (как это, например, делает BinaryReader при помощи BitConverter'а). A>Если нужно считать структуру, нужно считать массив байт, после чего отмарашалить его при помощи метода PtrToStructure.
Вообще-то для этого принято использовать memmapped files + ReadArray<T>. Либо чтение в буфер с последующим Marshal.Copy() как тут. Вам всё равно нужно два массива: один обрабатываем, в другой идёт асинхронное чтение, так что разницы никакой.
Ну и в крайнем случае можно использовать хак в моём сообщении выше, но вам оно не подойдёт — потеряете на синхронном чтении больше, чем сэкономите на копейках.
Re[7]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Albeoris, Вы писали:
A>Профит получил — выиграл 450 мсек. Это очень много, когда речь идёт о пользовательском интерфейсе, который должен максимально быстро и без видимых задержек реагировать на действия пользователя.
Вот тут ошибка. Такие вещи надо решать асинхронными чтением-обработкой. Иначе первый же не в меру любопытный антивирус, полусдохший бэдблок или "заснувший" винт и приплыли. Ну и разумеется, "крутилку" показывать с запозданием в полсекунды (если операция не смогла), а не сразу.
Re[6]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Sinix, Вы писали:
S>Вообще-то для этого принято использовать memmapped files + ReadArray<T>. Либо чтение в буфер с последующим Marshal.Copy() как тут. Вам всё равно нужно два массива: один обрабатываем, в другой идёт асинхронное чтение, так что разницы никакой.
Для чего "для этого"? MemoryMappedFile, естественно, используется. Что такое ReadArray<T>? Ещё раз: зачем читать в буфер, если можно читать напрямую в целевой массив?
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[8]: Изменение типа массива приводит к падению после сборки мусора
A>>Профит получил — выиграл 450 мсек. Это очень много, когда речь идёт о пользовательском интерфейсе, который должен максимально быстро и без видимых задержек реагировать на действия пользователя. S>Вот тут ошибка. Такие вещи надо решать асинхронными чтением-обработкой. Иначе первый же не в меру любопытный антивирус, полусдохший бэдблок или "заснувший" винт и приплыли. Ну и разумеется, "крутилку" показывать с запозданием в полсекунды (если операция не смогла), а не сразу.
Безусловно. Нужно. Будет сделана. Но это не решение проблемы, а костыль, который помогает пользователю понять, что мы занимаемся чем-то полезным, а не просто висим. Но я не вижу смысла "делать правильно и безопасно", если можно сделать "небезопасно, но быстро", сократив время ожидания с 1.5 секунд до 300 мсек.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[7]: Изменение типа массива приводит к падению после сборки мусора
A>Ещё раз: зачем читать в буфер, если можно читать напрямую в целевой массив?
Потому что вам важна производительность и потому что чтение и обработку предыдущего буфера можно выполнять параллельно.
A>Безусловно. Нужно. Будет сделана. Но это не решение проблемы, а костыль, который помогает пользователю понять, что мы занимаемся чем-то полезным, а не просто висим.
Это не костыль, а подстраховка на случай "никогда такого не было, и вот опять". Как показывает практика, обернуть код в самописный хелпер аля using (BeginUiOperation()) { ... } куда проще, чем краснеть на презентации из-за внезапно™ засбоившего %подставить%.
Re[8]: Изменение типа массива приводит к падению после сборки мусора
О, спасибо, возьму на заметку! Жаль, что его нет у ViewStream. Поменял вызовы в нескольких местах. К сожалению, не годится, когда файл нужно вначале распаковать.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[5]: Изменение типа массива приводит к падению после сборки мусора
Здравствуйте, Albeoris, Вы писали:
A>Здравствуйте, GlebZ, Вы писали:
A>Всё очень просто: для того чтобы считать массив Int32, нужно считать массив байт и декодировать его в Int32 (как это, например, делает BinaryReader при помощи BitConverter'а). A>Если нужно считать структуру, нужно считать массив байт, после чего отмарашалить его при помощи метода PtrToStructure. A>В обоих случая создаются два массива. В обоих случаях происходит копирование данных между ними. Когда речь идёт о 100 объектов, это не играет роли, но когда их миллионы, подобные телодвижения негативным образом сказываются на производительности.
Ты пытаешься писать на C# как на C. На C — массив это указатель, и указатели можно приводить одни к другому без копирования данных. На C# массив это не указатель. Если тебе нужен указатель, то используй указатели. То есть ты читаешь массив байт, а потом его превращаешь в указатель и работаешь с указателем.
То что ты хочешь получить на выходе — не массив структур, а указатель на структуру. Тебе ничего не мешает это сделать.
A>Вместо этого, я создаю массив структур (для простоты, возьмём Int32). Пришпиливаю его и меняю в памяти тип, заявляя, что массив на самом деле байтовый. При помощи упаковки подсовываю его в метод Stream.Read, который наполняет предоставленную область памяти байтами, после чего я меняю тип массива на первоначальный. Таким образом получая массив нужных мне структур без лишнего копирования и двойного выделения памяти.
И зачем тебе это? Чтобы ты смог написать Array.Sort к полученному массиву? Не смеши.
Для быстродействия ты все равно будешь:
1) Не использовать linq, поэтому тебе не нужна реализация IEnumerable
2) Напишешь обычный for-loop, поэтому внутри for не будет разницы у тебя массив или указатель
Re[9]: Изменение типа массива приводит к падению после сборки мусора
A>О, спасибо, возьму на заметку! Жаль, что его нет у ViewStream. Поменял вызовы в нескольких местах. К сожалению, не годится, когда файл нужно вначале распаковать.
Когда файл сначала надо распаковать, то используй UnmanagedMemoryStream, передавай ему указатель на массив структур, сконвертированный в Byte* и пиши в него при распаковке.
Re[7]: Изменение типа массива приводит к падению после сборки мусора
A>Профит получил — выиграл 450 мсек. Это очень много, когда речь идёт о пользовательском интерфейсе, который должен максимально быстро и без видимых задержек реагировать на действия пользователя.
Когда речь идет о пользовательском интерфейсе нужны асинхронные операции, слава богу во всех современных UI других нет. Лочить UI чтением даже на 100мс смерти подобно.
Самый лучший вариант — потоковая обработка и feedback и в UI. Даже если у тебя многогигабайтный файл, то тебе совершенно не требуется его обрабатывать весь. Ты можешь обработать кусок и сразу его отдать в UI.
Re: Изменение типа массива приводит к падению после сборки мусор