Главная » Хабрахабр » [Из песочницы] Компактный сериализатор для кэша c использованием System.Reflection.Emit

[Из песочницы] Компактный сериализатор для кэша c использованием System.Reflection.Emit

Держать в кэше информацию можно самую разную и в разной форме: и строки, и списки, и состояние сессии, и многое другое. В современных сервисах без кэша никуда: доступ к данным в персистентной базе – дело долгое и затратное, поэтому добавление промежуточного хранилища для наиболее часто используемых данных значительно его ускоряет. В данной статье речь пойдёт об одном из способов хранении в кэше «плоских» объектов, не имеющих вложенных классов и циклических ссылок.

Кастомизированная сериализация плоских объектов

Такие кэши как Redis Cache для Microsoft Azure не предлагают встроенной сериализации объектов, по крайней мере в составе клиентской библиотеки StackExchange.Redis. Кэш предоставляет методы, позволяющие сохранить под заданным ключом произвольную последовательность байт, а выбор способа их получения из сохраняемого объекта остаётся за пользователем. Одним из возможных вариантов является стандартный .Net-овский BinaryFormatter, однако он не единственный, и выбор удачного способа сериализации для сервиса, хранящего в кэше много объектов, может положительно сказаться на его производительности.

Подход к сериализации, описываемый в данной статье, исходит из следующих предположений о системе:

  • Кэш ограничен по объёму и стоит дороже персистентного хранилища. В Microsoft Azure за кэш с большим доступным лимитом ожидаемо приходится платить большую сумму.
  • Большое количество байт в сериализованном представлении негативно сказывается на времени взаимодействия кэшем, не только из-за затрат на запись/считывание, но также из-за передачи данных по сети.
  • Состав сериализуемых объектов стабилен во времени и находится под полным контролем владельца сервиса и кэша. Неожиданного появления, удаления или переупорядочивания полей не происходит, как это может быть с сериализованными данными, пришедшими извне.
  • Кэш — всего лишь средство оптимизации и временное хранилище объектов. При расхождении формата или проблемах десериализации удаление старого объекта из кэша и замена новым не будет представлять собой большую проблему

На фоне этого привлекательной идеей кажется экономия изрядного количества байт за счёт исключения из сериализованного представления метаданных о полях, классах и сборках, сохраняемых всеми штатными сериализаторами, включая BinaryFormatter. В конце концов, если владелец всё знает о составе полей, что мешает, например, просто сформатировать их в строчку в определённом порядке, разделив каким-либо заведомо не встречающимся в их составе символом?

"7c9e6679-7425-40de-944b-e07fc1f90ae7|48972|Alice in Wonderland"

Хотя такой подход и требует некоторой аккуратности, пагубным его не назовёшь.
Если пойти ещё дальше, то можно обойтись и без разделителя, и без форматирования в строку: любой тип представляется в памяти последовательностью байт, и, зная тип того или иного свойства, можно просто поочерёдно записывать и считывать массивы байт нужной длины, конвертируя их из/в значения нужных типов. Для типов с нефиксированной длинной, таких как string или Array, можно предварять сериализованное значение количеством байт/элементов, которые в нём содержатся. Со свойствами, имеющими тип пользовательского класса или структуры дело обстоит сложнее, особенно если нужно отслеживать циклические ссылки и в данном простейшем случае они пока не рассматриваются. (Это, однако, не означает, что подобное нереализуемо в принципе).

Если объекты нужно сохранять целиком, то подобная операция легко автоматизируется средствами пространства имён System. При хранении в кэше большого количества объектов с большим количеством полей написание кода их сериализации по описанному принципу может быть утомительным и чреватым ошибками. Reflection:

public override void Serialize(TObject theObject, Stream stream)
else if (property.PropertyType == typeof(bool)) { var bytes = BitConverter.GetBytes((bool)val); stream.Write(bytes, 0, bytes.Length); } else if (property.PropertyType == typeof(int)) { var bytes = BitConverter.GetBytes((int)val); stream.Write(bytes, 0, bytes.Length); } else if (property.PropertyType == typeof(Guid)) { var bytes = ((Guid)val).ToByteArray(); stream.Write(bytes, 0, bytes.Length); } ... }
} public override TObject Deserialize(Stream stream)
{ var theObject = Activator.CreateInstance<TObject>(); foreach (var property in _properties) { object val; if (property.PropertyType == typeof(byte)) { val = stream.ReadByte(); } var bytesCount = TypesInfo.GetBytesCount(type); var valueBytes = new byte[bytesCount]; stream.Read(valueBytes, 0, valueBytes.Length); if (property.PropertyType == typeof(bool)) { val = BitConverter.ToBoolean(valueBytes, 0); } else if (property.PropertyType == typeof(int)) { val = BitConverter.ToInt32(valueBytes, 0); } else if (property.PropertyType == typeof(Guid)) { val = new Guid(valueBytes); } ... property.SetValue(theObject, val); }
}

Состав свойств объекта и его изменения

Несмотря на то, что MSDN не гарантирует возврат свойств методом Type.GetProperties() в алфавитном порядке или порядке декларирования, нет причин полагать, что от вызова к вызову на одной и той же версии одного и того же типа объекта возвращаемый массив будет отличаться. Для большей надёжности можно один раз вызвать данный метод, сохранить полученный массив свойств в приватном поле и использовать его далее и в операциях сериализации, и десериализации. Кэш обычно используется системами, выполняющимися без остановки длительное время, и единожды созданный таким образом сериализатор с единожды проинициализированным списком свойств сериализуемого типа будет существовать долго. В случае, если и это кажется недостаточным, можно было бы дополнительно реализовать сохранение этого списка на диск с переинициализацией при рестарте сервиса, но это выглядит излишний предосторожностью.

Однако, поскольку описываемая идея предназначена для сохранения данных в кэше, а не постоянной базе данных, наиболее простой способ справится с возникшим расхождением — это инвалидировать старое байтовое представление и заменить на заново сериализованный объект. Тем не менее, при изменении состава свойств, их названия, типа или взаимного расположения корректно десериализовать старый вариант уже не представляется возможным. Возможны и более сложные подходы, например, при помощи отдельного AppDomain-а загрузить старый вариант типа, десериализовать объект, заполнить его свойствами новый объект, сериализовать и сохранить его под тем же ключом; в рамках данной статьи, однако, попыток выполнить подобное не предпринималось.

Например, перед байтами самого объекта может быть записана версия сборки, в которой он объявлен: В любом случае, для отслеживания изменений в типе объекта, процесс сериализации должен включать возможность версионирования.

public virtual string GetTypeVersion()
{ return typeof(TObject).Assembly.GetName().Version.ToString();
}


var reflectionSerializer = new ReflectionCompactSerializer<Entity>();
typeVersion = reflectionSerializer.GetTypeVersion(); reflectionSerializer.WriteVersion(stream, typeVersion);
reflectionSerializer.Serialize(originalEntity, stream); var version = reflectionSerializer.ReadObjectVersion(stream);
deserializedEntity = reflectionSerializer.Deserialize(stream);

При сериализации множества разных объектов, для каждого из которых метод GetProperties() вызывается только один раз с сохранением результата в памяти, необходимо как-то сопоставлять типы объектов с полученными списками свойств. Для этого можно либо использовать словарь Type → PropertyInfo[], либо выделять специализированные сериализаторы для каждого сериализуемого типа при помощи Generics. Второй подход субъективно выглядит более удобным:

public class ReflectionCompactSerializer<TObject> : CompactSerializerBase<TObject> where TObject: class, new ()
{ private readonly PropertyInfo[] _properties = typeof(TObject).GetProperties(BindingFlags.Instance | BindingFlags.Public); ...
}

Более производительный подход

Следующая проблема, возникающая при использовании Reflection в разрезе данной задачи — это опять же производительность: отражение никогда не считалось быстрым механизмом. В таких книгах по оптимизации .Net-приложений как Sasha Goldshtein, Dima Zurbalev, Ido Flatow «Pro .Net Performance: Optimize Your C# Applications» и Ben Watson «Writing High-Performance .NET Code» в качестве одной из методик оптимизации при работе с Reflection и создании пользовательских сериализаторов предлагается генерация кода, например при помощи средств пространства имен System.Reflection.Emit. Идея при таком подходе состоит в том, чтобы по полученному списку свойств создавать сразу код, т. е. последовательность инструкций, которая будет поочерёдно получать значение каждого из свойств, писать его в поток байт, считывать, преобразовывать, устанавливать значение и т. п.

Reflection. Класс ILGenerator из пространства имён System. В общих чертах это выглядит так: Emit содержит ряд методов, позволяющих создавать инструкции промежуточного языка MSIL, которые после этого могут быть скомпилированы в run-time при помощи класса DynamicMethod.

public static EmitSerializer<TObject> Generate<TObject>() where TObject : class, new()
{ var propertiesWriter = new DynamicMethod( "WriteProperties", null, new Type[] { typeof(Stream), typeof(TObject) }, typeof(EmitSerializer<TObject>)); var writerIlGenerator = propertiesWriter.GetILGenerator(); var writerEmitter = new CodeEmitter(writerIlGenerator); var propertiesReader = new DynamicMethod( "ReadProperties", null, new Type[] { typeof(Stream), typeof(TObject) }, typeof(EmitSerializer<TObject>)); var readerIlGenerator = propertiesReader.GetILGenerator(); var readerEmitter = new CodeEmitter(readerIlGenerator); var properties = typeof(TObject) .GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach(var property in properties) { if (property.PropertyType == typeof(byte)) { writerEmitter.EmitWriteBytePropertyCode(property); readerEmitter.EmitReadBytePropertyCode(property); } else if (property.PropertyType == typeof(Guid)) { writerEmitter.EmitWriteGuidPropertyCode(property); readerEmitter.EmitReadGuidPropertyCode(property); } … } var writePropertiesDelegate = (Action<Stream,TObject>)propertiesWriter .CreateDelegate(typeof(Action<Stream, TObject>)); var readPropertiesDelegate = (Action<Stream, TObject>)propertiesReader .CreateDelegate(typeof(Action<Stream, TObject>)); return new EmitSerializer<TObject>( writePropertiesDelegate, readPropertiesDelegate); }
}

Разумеется, за пределами «общих черт», наиболее сложная и интересная часть заключается в реализации методов EmitWriteNNNPropertyCode/EmitReadNNNPropertyCode.

Emit(OpCode). MSIL — это «высокоуровневый ассемблер» и код на нём порой читать сложно, не то что писать, особенно опосредованным способом через вызов методов ILGenerator.

Вполне можно создать «заготовку» на C#, создать с ней сборку, дизассемблировать в IL и, глядя на полученную эталонную реализацию, обобщить её в соответствии со своими нуждами. Тут помогает уловка, приведённая в одной из вышеупомянутых книг: не обязательно писать код на IL целиком «с нуля».

Net-сборки IL-код существует большое количество: ildasm, dotPeek, ILSpy и т. Дизассемблеров, позволяющих под Windows получить из . Так получилось, однако, что данный проект, начатый под ОС от Microsoft, дописывался уже под Linux (благо . д. Тем не менее, инструменты имеются и под эту операционную систему, в частности monodis. NET Core позволяет), где выбор дизассемблеров не так велик. Получить текстовый файл и исходным кодом на IL из dll-сборки при помощи monodis можно следующей командой:

monodis <путь к сборке> --output=<путь к выходному файлу>

Обобщённая сериализация простейших типов

Все действия, выполняемые генерируемым сериализатором, аналогичны операциям изначального Reflection-сериализатора и повторить их через Emit проще, чем может показаться на первый взгляд. Например, «заготовка», получающая значение свойства типа int и записывающая его байты в поток, может выглядеть так:

private static void WritePrimitiveTypeProperty(Stream stream, Entity entity)
{ var index = entity.Index; var valueBytes = BitConverter.GetBytes(index); stream.Write(valueBytes, 0, valueBytes.Length);
}

После сборки и декомпиляции, соответствующий IL-код будет содержать следующие инструкции:

.method private static hidebysig default void WritePrimitiveTypeProperty (class [mscorlib]System.IO.Stream 'stream', class SourcesForIL.Entity entity) cil managed { // Method begins at RVA 0x241c // Code size 28 (0x1c) .maxstack 4 .locals init ( int32 V_0, unsigned int8[] V_1) IL_0000: nop IL_0001: ldarg.1 IL_0002: callvirt instance int32 class SourcesForIL.Entity::get_Index() IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: call unsigned int8[] class [mscorlib]System.BitConverter::GetBytes(int32) IL_000e: stloc.1 IL_000f: ldarg.0 IL_0010: ldloc.1 IL_0011: ldc.i4.0 IL_0012: ldloc.1 IL_0013: ldlen IL_0014: conv.i4 IL_0015: callvirt instance void class [mscorlib]System.IO.Stream::Write(unsigned int8[], int32, int32) IL_001a: nop IL_001b: ret }

А код, повторяющий его средствами пространства имён Reflection.Emit, вызывает такую последовательность методов ILGenerator-а:

var byteArray = _ilGenerator.DeclareLocal(typeof(byte[])); // load object under serialization onto the evaluation stack
_ilGenerator.Emit(OpCodes.Ldarg_1);
// get property value
_ilGenerator.EmitCall(OpCodes.Callvirt, property.GetMethod, null); // get value's representation in bytes
_ilGenerator.EmitCall(OpCodes.Call, BitConverterMethodsInfo.ChooseGetBytesOverloadByType(valueType), null);
// save the bytes array from the stack in local variable
_ilGenerator.Emit(OpCodes.Stloc, byteArray); // load stream parameter onto the evaluation stack
_ilGenerator.Emit(OpCodes.Ldarg_0);
// load bytesCount array
_ilGenerator.Emit(OpCodes.Ldloc_S, bytesArray);
// load offset parameter == 0 onto the stack
_ilGenerator.Emit(OpCodes.Ldc_I4_0); // load bytesCount array
_ilGenerator.Emit(OpCodes.Ldloc_S, bytesArray);
// calculate the array length
_ilGenerator.Emit(OpCodes.Ldlen);
// convert it to Int32
_ilGenerator.Emit(OpCodes.Conv_I4);
// write array to stream
_ilGenerator.EmitCall(OpCodes.Callvirt, StreamMethodsInfo.Write, null);

Следует обратить внимание, что получение значения свойства происходит единообразно для всех типов – при помощи property.GetMethod. Точно также легко обобщается преобразование этого значения в массив байт: нужно лишь использовать подходящий аргумент типа MethodInfo. Таким образом, одна и та же функция генератора может создавать код, сериализующий свойства разных типов, в зависимости от переданного ей метода получения массива байт.
Класс System.BitConverter содержит несколько перегруженных методов GetBytes, для нескольких строенных типов, и для всех этих типов операция сериализации может быть произведена единообразно, за счёт выбора нужного варианта MethodInfo в BitConverterMethodsInfo.ChooseGetBytesOverloadByType(valueType).

Получать объект MedodInfo для соответствующих методов приходится снова через Reflection и для более быстрого к ним доступа имеет смысл сохранять их в статическом словаре:

public static MethodInfo ChooseGetBytesOverloadByType(Type type)
{ if (_getBytesMethods.ContainsKey(type)) { return _getBytesMethods[type]; } var method = typeof(BitConverter).GetMethod("GetBytes", new Type[] { type }); if (method == null) { throw new InvalidOperationException("No overload for parameter of type " + type.Name); } _getBytesMethods[type] = method; return method;
}

Вышеприведённый код позволяет генерировать код для нескольких системных типов: bool, short, int, long, ushort, uint, ulong, double, float, char.

Сериализация decimal, guid и byte

Из списка примитивных типов выбивается тип decimal, для которого в System.BitConverter нет встроенного метода получения массива байт. Поэтому методы преобразования приходится реализовывать самостоятельно:

public static byte[] GetDecimalBytes(decimal value)
{ var bits = decimal.GetBits((decimal)value); var bytes = new List<byte>(); foreach (var bitsPart in bits) { bytes.AddRange(BitConverter.GetBytes(bitsPart)); } return bytes.ToArray(); } public static decimal BytesToDecimal(byte[] bytes, int startIndex)
{ var valueBytes = bytes.Skip(startIndex).ToArray(); if (valueBytes.Length != 16) throw new Exception("A decimal must be created from exactly 16 bytes"); var bits = new Int32[4]; for (var bitsPart = 0; bitsPart <= 15; bitsPart += 4) { bits[bitsPart/4] = BitConverter.ToInt32(valueBytes, bitsPart); } return new decimal(bits); }

Для типа Guid перегрузка метода BitConverter.GetBytes так же отсутствует, но получение его байтового представления тривиально — при помощи метода Guid.ToByteArray. Для восстановления значения из массива байт у Guid есть конструктор.

private static void WriteGuidProperty(Stream stream, Entity entity)
{ var id = entity.Id; var valueBytes = id.ToByteArray(); stream.Write(valueBytes, 0, valueBytes.Length);
} private static void ReadGuidProperty(Stream stream, Entity entity)
{ var valueBytes = new byte[16]; stream.Read(valueBytes, 0, valueBytes.Length); entity.Id = new Guid(valueBytes);
}

С byte дела обстоят совсем просто: класс Stream имеет специальные методы записи-чтения единичного байта.

Сериализация даты и времени

Чуть сложнее дело обстоит с типами DateTime и DateTimeOffset, поскольку они, определяются не только значением времени, но ещё и полями Kind и Offset соответственно; эти поля нужно записать/считать наряду с самим временем. Генерация IL-кода, сохраняющего значение переменной DateTimeOffset, например, выглядит так:

private void EmitWriteDateTimeOffsetVariable(LocalBuilder dateTimeOffset)
{ var offset = _ilGenerator.DeclareLocal(typeof(TimeSpan)); var dateTimeTicks = _ilGenerator.DeclareLocal(typeof(long)); var dateTimeTicksByteArray = _ilGenerator.DeclareLocal(typeof(byte[])); var offsetTicksByteArray = _ilGenerator.DeclareLocal(typeof(byte[])); // load the variable address to the stack _ilGenerator.Emit(OpCodes.Ldloca_S, dateTimeOffset); // call method to get Offset property _ilGenerator.EmitCall(OpCodes.Call, DateTimeOffsetMembersInfo.OffsetProperty, null); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, offset); // load the variable address to the stack _ilGenerator.Emit(OpCodes.Ldloca_S, offset); // call method to get offset Ticks property _ilGenerator.EmitCall(OpCodes.Call, TimeSpanMembersInfo.TicksProperty, null); // convert it to byte array _ilGenerator.EmitCall(OpCodes.Call, GetInt64BytesMethodInfo, null); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, offsetTicksByteArray); EmitWriteBytesArrayToStream(offsetTicksByteArray); // load the dateTimeOffset variable address to the stack _ilGenerator.Emit(OpCodes.Ldloca_S, dateTimeOffset); // call method to get Ticks property _ilGenerator.EmitCall(OpCodes.Call, DateTimeOffsetMembersInfo.TicksProperty, null); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, dateTimeTicks); // load the variable address to the stack _ilGenerator.Emit(OpCodes.Ldloc, dateTimeTicks); // convert it to byte array _ilGenerator.EmitCall(OpCodes.Call, GetInt64BytesMethodInfo, null); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, dateTimeTicksByteArray); EmitWriteBytesArrayToStream(dateTimeTicksByteArray);
}

Определение длины типа в байтах

Десериализация всех вышеперечисленных типов симметрична, однако для считывания соответствующего массива байт, необходимо знать его длину. Во время выполнения тип свойства доступен как значение класса Type, и применить к нему операцию sizeof не получится. На помощь приходит метод Marshal.SizeOf, который, однако, применим не ко всем типам, и уж тем более не возвращает количество байт, записываемых кастомными реализациями сохранения. Для них, однако, можно просто явно возвращать размер:

public static int GetBytesCount(Type propertyType)
{ if (propertyType == typeof(DateTime)) { return sizeof(long) + 1; } else if (propertyType == typeof(DateTimeOffset)) { return sizeof(long) + sizeof(long); } else if (propertyType == typeof(bool)) { return sizeof(bool); } else if(propertyType == typeof(char)) { return sizeof(char); } else if (propertyType == typeof(decimal)) { return 16; } else { return Marshal.SizeOf(propertyType); }
}

Сериализация Nullable<T>

При сериализации Nullable логично записывать вначале флаг, определяющий, содержит ли свойство null, и только если значение не пусто, записывать само значение, при помощи уже реализованных методов.

public void EmitWriteNullablePropertyCode(PropertyInfo property)
{ var nullableValue = _ilGenerator.DeclareLocal(property.PropertyType); var isNull = _ilGenerator.DeclareLocal(typeof(bool)); var isNullByte = _ilGenerator.DeclareLocal(typeof(byte)); var underlyingType = property.PropertyType.GetGenericArguments().Single(); var value = _ilGenerator.DeclareLocal(underlyingType); var valueBytes = _ilGenerator.DeclareLocal(typeof(byte[])); var nullableInfo = NullableInfo.GetNullableInfo(underlyingType); var nullFlagBranch = _ilGenerator.DefineLabel(); var byteFlagLabel = _ilGenerator.DefineLabel(); var noValueLabel = _ilGenerator.DefineLabel(); EmitLoadPropertyValueToStack(property); // save nullable value to local variable _ilGenerator.Emit(OpCodes.Stloc, nullableValue); // load address of the variable to stack _ilGenerator.Emit(OpCodes.Ldloca_S, nullableValue); // get HasValue property _ilGenerator.EmitCall(OpCodes.Call, nullableInfo.HasValueProperty, null); // load value '0' to stack _ilGenerator.Emit(OpCodes.Ldc_I4_0); // compare _ilGenerator.Emit(OpCodes.Ceq); // save to local boolean variable _ilGenerator.Emit(OpCodes.Stloc, isNull); // load to stack _ilGenerator.Emit(OpCodes.Ldloc, isNull); // jump to isNull branch, if needed _ilGenerator.Emit(OpCodes.Brtrue_S, nullFlagBranch); // load value '0' to stack _ilGenerator.Emit(OpCodes.Ldc_I4_0); // jump to byteFlagLabel _ilGenerator.Emit(OpCodes.Br_S, byteFlagLabel); _ilGenerator.MarkLabel(nullFlagBranch); // load value '1' to stack _ilGenerator.Emit(OpCodes.Ldc_I4_1); _ilGenerator.MarkLabel(byteFlagLabel); // convert to byte _ilGenerator.Emit(OpCodes.Conv_U1); // save to local variable _ilGenerator.Emit(OpCodes.Stloc, isNullByte); // load stream parameter to stack _ilGenerator.Emit(OpCodes.Ldarg_0); // load byte flag to the stack _ilGenerator.Emit(OpCodes.Ldloc, isNullByte); // write it to the stream _ilGenerator.EmitCall(OpCodes.Callvirt, StreamMethodsInfo.WriteByte, null); // load isNull flag to stack _ilGenerator.Emit(OpCodes.Ldloc, isNull); // load value '0' _ilGenerator.Emit(OpCodes.Ldc_I4_0); // compare _ilGenerator.Emit(OpCodes.Ceq); // jump to tne end, if no value presented _ilGenerator.Emit(OpCodes.Brfalse_S, noValueLabel); // load the address of the nullable to the stack _ilGenerator.Emit(OpCodes.Ldloca_S, nullableValue); // get actual value _ilGenerator.EmitCall(OpCodes.Call, nullableInfo.ValueProperty, null); EmitWriteValueFromStackToStream(underlyingType); _ilGenerator.MarkLabel(noValueLabel);
}

Обработка типа string

Значения типа string не имеют фиксированного размера и количество хранимых в них байт заранее неизвестно. Тем не менее, длину конкретной строки можно сохранить в поток перед записью составляющих её байт. При десериализации же можно вначале считать массив байт, содержащих int с длиной строки, получить значение этой длины и далее читать уже соответствующее ей количество байт. В случае null-строки, можно записать значение «-1», чтобы уметь отличать её от пустой строки длинной 0.

GetBytes/Encoding. Преобразование строки в массив байт/из него легко выполняется посредством методов Encoding. Для разнообразия приведён метод чтения строки из потока, а не записи: GetString.

private void EmitReadStringFromStreamToStack()
{ var bytesCoutArray = _ilGenerator.DeclareLocal(typeof(byte[])); var stringBytesCount = _ilGenerator.DeclareLocal(typeof(int)); var stringBytesArray = _ilGenerator.DeclareLocal(typeof(byte[])); var isNull = _ilGenerator.DeclareLocal(typeof(bool)); var isNotNullBranch = _ilGenerator.DefineLabel(); var endOfReadLabel = _ilGenerator.DefineLabel(); var propertyBytesCount = TypesInfo.GetBytesCount(typeof(int)); // push the amout of bytes to read onto the stack _ilGenerator.Emit(OpCodes.Ldc_I4, propertyBytesCount); // allocate array to store bytes _ilGenerator.Emit(OpCodes.Newarr, typeof(byte)); // stores the allocated array in the local variable _ilGenerator.Emit(OpCodes.Stloc, bytesCoutArray); // push the stream parameter _ilGenerator.Emit(OpCodes.Ldarg_0); // push the byte count array _ilGenerator.Emit(OpCodes.Ldloc, bytesCoutArray); // push '0' as the offset parameter _ilGenerator.Emit(OpCodes.Ldc_I4_0); // push the byte array again - to calculate its length _ilGenerator.Emit(OpCodes.Ldloc, bytesCoutArray); // get the length _ilGenerator.Emit(OpCodes.Ldlen); // convert the result to Int32 _ilGenerator.Emit(OpCodes.Conv_I4); // call the stream.Read method _ilGenerator.EmitCall(OpCodes.Callvirt, StreamMethodsInfo.Read, null); // pop amount of bytes read _ilGenerator.Emit(OpCodes.Pop); // push the bytes count array _ilGenerator.Emit(OpCodes.Ldloc, bytesCoutArray); // push '0' as the start index parameter _ilGenerator.Emit(OpCodes.Ldc_I4_0); // convert the bytes to Int32 _ilGenerator.EmitCall(OpCodes.Call, BytesToInt32MethodInfo, null); // save bytes count to local variable _ilGenerator.Emit(OpCodes.Stloc, stringBytesCount); // load it to the stack _ilGenerator.Emit(OpCodes.Ldloc, stringBytesCount); // put value '-1' to the stack _ilGenerator.Emit(OpCodes.Ldc_I4_M1); // compare bytes count and -1 _ilGenerator.Emit(OpCodes.Ceq); // save to boolean variable _ilGenerator.Emit(OpCodes.Stloc, isNull); // load to stack _ilGenerator.Emit(OpCodes.Ldloc, isNull); // if false, jump to isNotNullBranch _ilGenerator.Emit(OpCodes.Brfalse_S, isNotNullBranch); // push 'null' value _ilGenerator.Emit(OpCodes.Ldnull); // jump to the end of read fragment _ilGenerator.Emit(OpCodes.Br_S, endOfReadLabel); // not null string value branch _ilGenerator.MarkLabel(isNotNullBranch); // load bytes count to the stack _ilGenerator.Emit(OpCodes.Ldloc, stringBytesCount); // allocate array to store bytes _ilGenerator.Emit(OpCodes.Newarr, typeof(byte)); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, stringBytesArray); // push the stream parameter _ilGenerator.Emit(OpCodes.Ldarg_0); // load string bytes array to stack _ilGenerator.Emit(OpCodes.Ldloc, stringBytesArray); // push '0' as the start index parameter _ilGenerator.Emit(OpCodes.Ldc_I4_0); // load string bytes array to stack to get array length _ilGenerator.Emit(OpCodes.Ldloc, stringBytesArray); // get the length _ilGenerator.Emit(OpCodes.Ldlen); // convert the result to Int32 _ilGenerator.Emit(OpCodes.Conv_I4); // call the stream.Read method _ilGenerator.EmitCall(OpCodes.Callvirt, StreamMethodsInfo.Read, null); // pop amount of bytes read _ilGenerator.Emit(OpCodes.Pop); // load Encoding to stack _ilGenerator.EmitCall(OpCodes.Call, EncodingMembersInfo.EncodingGetter, null); // load string bytes _ilGenerator.Emit(OpCodes.Ldloc, stringBytesArray); // call Encoding.GetString() method _ilGenerator.EmitCall(OpCodes.Callvirt, EncodingMembersInfo.GetStringMethod, null); _ilGenerator.MarkLabel(endOfReadLabel);
}

Работа с массивами и коллекциями

Подходом, аналогичным работе со строками можно воспользоваться при сериализации массива: вначале записывается (считывается) его длина, а затем в цикле с соответствующим количеством итераций, поочерёдно записывается (считывается) каждый элемент, при помощи одного из реализованных выше способов обработки отдельного значения.

public void EmitReadArrayPropertyCode(PropertyInfo property)
{ var elementType = property.PropertyType.GetElementType(); var elementBytesArray = _ilGenerator.DeclareLocal(typeof(byte[])); var lengthBytes = _ilGenerator.DeclareLocal(typeof(byte[])); var arrayLength = _ilGenerator.DeclareLocal(typeof(int)); var array = _ilGenerator.DeclareLocal(property.PropertyType); var element = _ilGenerator.DeclareLocal(elementType); var index = _ilGenerator.DeclareLocal(typeof(int)); var isNullArrayLabel = _ilGenerator.DefineLabel(); var setPropertyLabel = _ilGenerator.DefineLabel(); var loopConditionLabel = _ilGenerator.DefineLabel(); var loopIterationLabel = _ilGenerator.DefineLabel(); // push deserialized object to stack _ilGenerator.Emit(OpCodes.Ldarg_1); EmitAllocateBytesArrayForType(typeof(int), lengthBytes); EmitReadByteArrayFromStream(lengthBytes); EmitConvertBytesArrayToPrimitiveValueOnStack(lengthBytes, typeof(int)); // save it to local variable _ilGenerator.Emit(OpCodes.Stloc, arrayLength); EmitJumpIfNoElements(arrayLength, isNullArrayLabel); // push array length to stack _ilGenerator.Emit(OpCodes.Ldloc, arrayLength); // create new array _ilGenerator.Emit(OpCodes.Newarr, elementType); // save it to the local variable _ilGenerator.Emit(OpCodes.Stloc, array); EmitZeroIndex(index); if (elementType != typeof(string)) { EmitAllocateBytesArrayForType(elementType, elementBytesArray); } // jump to the loop condition check _ilGenerator.Emit(OpCodes.Br_S, loopConditionLabel); _ilGenerator.MarkLabel(loopIterationLabel); if (elementType == typeof(string)) { EmitReadStringFromStreamToStack(); } else { EmitReadValueFromStreamToStack(elementType, elementBytesArray); } // save to local variable _ilGenerator.Emit(OpCodes.Stloc, element); // load array instance to stack _ilGenerator.Emit(OpCodes.Ldloc, array); // load element index _ilGenerator.Emit(OpCodes.Ldloc_S, index); // load the element to stack _ilGenerator.Emit(OpCodes.Ldloc_S, element); // set element to the array _ilGenerator.Emit(OpCodes.Stelem, elementType); EmitIndexIncrement(index); _ilGenerator.MarkLabel(loopConditionLabel); EmitIndexIsLessCheck(index, arrayLength); // jump to the iteration if true _ilGenerator.Emit(OpCodes.Brtrue_S, loopIterationLabel); // push filled array to stack _ilGenerator.Emit(OpCodes.Ldloc, array); // jump to SetProperty label _ilGenerator.Emit(OpCodes.Br_S, setPropertyLabel); _ilGenerator.MarkLabel(isNullArrayLabel); _ilGenerator.Emit(OpCodes.Ldnull); _ilGenerator.MarkLabel(setPropertyLabel); // call object's property setter _ilGenerator.EmitCall(OpCodes.Callvirt, property.SetMethod, null);
}

Помимо массивов, в проекте так же реализованы Generic-коллекции. Тестирование проводилось на списках List, но код спроектирован так, чтобы обрабатывалось любое свойство-коллекция, если оно удовлетворяет следующим условиям:

  • имеет публичный конструктор без параметров;
  • реализует интерфейс ICollection<T>;
  • T является простым типом, рассмотренным ранее;

Сериализация и десериализация коллекций реализована сходным с массивами образом, с поправкой на использование методов и свойств Add, Count, GetEnumerator, имеющихся вследствие реализации интерфейса ICollection, а так же за счёт вызовов MoveNext и Current у полученного Enumerator-a.

Тестирование, сравнение, выводы

Данное исследование было бы неполным без сравнения полученных сериализаторов со штатными вариантами. Для оценки выигрыша были выбраны упомянутый BinaryFormatter и Newtonsoft JsonSerializer, как одни из самых популярных библиотечных реализаций. Xml-сериализация не рассматривалась, поскольку она заведомо ещё более «многословна». Сравнение производилось по усреднённому из 1000 попыток времени сериализации/десериализации, а также размеру сериализованного представления в байтах. Объект для эксперимента включал свойства всех вышеупомянутых типов, и не содержал свойств, не поддерживаемых данной реализацией:

var originalEntity = new Entity
{ Name = "Name", ShortName = string.Empty, Description = null, Label = 'L', Age = 32, Index = -7, IsVisible = true, Price = 225.87M, Rating = 4.8, Weigth = 130, ShortIndex = short.MaxValue, LongIndex = long.MinValue, UnsignedIndex = uint.MaxValue, ShortUnsignedIndex = 25, LongUnsignedIndex = 11, Id = Guid.NewGuid(), CreatedAt = DateTime.Now, CreatedAtUtc = DateTime.UtcNow, LastAccessed = DateTime.MinValue, ChangedAt = DateTimeOffset.Now, ChangedAtUtc = DateTimeOffset.UtcNow, References = null, Weeks = new List<short>() { 3, 12, 24, 48, 53, 61 }, PricesHistory = new decimal[] { 225.8M, 226M, 227.87M, 224.87M }, BitMap = new bool[] { true, true, false, true, false, false, true, true }, ChildrenIds = new Guid [] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }, Schedule = new DateTime [] { DateTime.Now.AddDays(-1), DateTime.Now.AddMonths(2), DateTime.Now.AddYears(10) }, Moments = new DateTimeOffset [] { DateTimeOffset.UtcNow.AddDays(-5), DateTimeOffset.Now.AddDays(10) }, Tags = new List<string> { "The quick brown fox jumps over the lazy dog", "Reflection.Emit", string.Empty, "0" }, AlternativeId = Guid.NewGuid()
};

EmitSerializer имел ещё более высокие результаты (без учёта времени разовой компиляции сгенерированного кода). Нужно отметить, что на таком объекте даже «чистый» Reflection-сериализатор показал результаты лучше, чем штатные варианты, очевидно, из-за их ориентированности на более общие и сложные задачи. Значения, полученные в ходе замеров:

Serializer | Average elapsed, ms | Size, bytes
-------------------------------------------------------------------------------
EmitSerializer | 9.9522 | 477
-------------------------------------------------------------------------------
ReflectionSerializer | 22.9454 | 477
-------------------------------------------------------------------------------
BinaryFormatter | 246.4836 | 1959
-------------------------------------------------------------------------------
Newtonsoft JsonSerializer | 87.1893 | 1156 EmitSerializer compiled in: 104.5019 ms

Исходный код

Исходники решения можно найти на Github.

С проекта, в рамках которого возникла данная идея, автор на данный момент ушёл, и возможности провести испытания в «боевых условиях» не имел. Реализация, однако, предоставляется as is, без гарантий безошибочности и надёжности.

NET Core 2. Код написан под . 04 LTS. 0, собирался и тестировался на ОС Linux Ubuntu 16.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Профессиональные навыки, востребованные среди UX-специалистов (срез 2018)

С 29 августа по 07 сентября 2018 сообщество UX SPb (независимое сообщество UX-специалистов Санкт-Петербурга) проводило опрос, направленный на изучение профессиональных навыков специалистов по пользовательским интерфейсам. Сообщество обещало опубликовать результаты. Обещание исполнено 🙂 В исследовании приняли участие 109 респондентов. Опрос проводился ...

От антикварного радио до DIY-колонок: 12 каналов на YouTube про устройство акустики

Сегодня мы подготовили подборку YouTube-каналов, авторы которых рассказывают об устройстве винтажного и современного аудиооборудования: от настройки виниловых проигрывателей до сборки акустических систем. Всех, кому это интересно, приглашаем к просмотру под кат. Фото Nathan Duprey / CC Канал посвящен сборке акустических ...