Главная » Хабрахабр » [Из песочницы] Компактный сериализатор для кэша 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 Интересное!

[Перевод] UDB. Что же это такое?

Как я уже отметил в статье, про UDB кратко не написать, но можно просто перевести фирменную документацию на него, чтобы познакомить отечественного читателя со столь мощным средством для реализации микропрограммных функций.Но прежде, давайте рассмотрим, что вообще такое PSoC. В комментариях ...

Беспроводные технологии передачи звука на базе Bluetooth: что же лучше?

С развитием технологий так привычные всем «ламповые» аналоговые наушники уходят в историю – их всё больше вытесняют беспроводные собратья на базе Bluetooth. Современные смартфоны лишаются привычного разъёма в угоду влаго- и пылезащищённости. Разработчики выпускают всё новые версии протокола Bluetooth и ...