📜 ⬆️ ⬇️

Compact serializer for cache using System.Reflection.Emit



In modern services without a cache, nowhere: data access in a persistent database is a long and expensive business, so adding intermediate storage for the most frequently used data speeds it up considerably. Caching information can be very different and in different forms: strings, and lists, and session state, and much more. In this article we will discuss one of the ways to store in the cache "flat" objects that do not have nested classes and circular references.

Customized flat object serialization


Caches like Redis Cache for Microsoft Azure do not offer built-in serialization of objects, at least as part of the StackExchange.Redis client library. The cache provides methods that allow you to save an arbitrary sequence of bytes under a given key, and the user remains to choose how to get them from the saved object. One of the possible options is the standard .Net-ovsky BinaryFormatter, but it is not the only one, and choosing a successful serialization method for a service that stores many objects in the cache can have a positive effect on its performance.

The serialization approach described in this article comes from the following assumptions about the system:
')

Against this background, an attractive idea seems to be saving a fair amount of bytes due to the exclusion from the serialized view of metadata about fields, classes and assemblies maintained by all regular serializers, including the BinaryFormatter. In the end, if the owner knows everything about the composition of the fields, what prevents, for example, simply formatting them into lines in a certain order, dividing them with some symbol that is not known to be found in their composition?

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

Although this approach requires some accuracy, you cannot call it disastrous.
If you go further, you can do without a separator, and without formatting into a string: any type is represented in memory as a sequence of bytes, and knowing the type of a particular property, you can simply write and read byte arrays of the required length alternately and convert them from / into the values ​​of the desired types. For types with non-fixed length, such as string or Array, you can precede the serialized value by the number of bytes / elements that it contains. It is more difficult to deal with properties that have the type of user class or structure, especially if you need to keep track of circular references and in this simplest case they are not considered yet. (This, however, does not mean that this is unrealizable in principle).

When storing a large number of objects in a cache with a large number of fields, writing code to serialize them according to the described principle can be tedious and fraught with errors. If objects need to be saved entirely, then such an operation can be easily automated by means of the System.Reflection namespace:

 public override void Serialize(TObject theObject, Stream stream) { foreach (var property in _properties) { var val = property.GetValue(theObject); if (property.PropertyType == typeof(byte)) { stream.WriteByte((byte)val); } 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); } } 

The composition of the properties of the object and its changes


Although MSDN does not guarantee that Type.GetProperties () will return properties in alphabetical or declaration order, there is no reason to believe that the returned array will differ from a call to a call on the same version of the same object type. For greater reliability, you can call this method once, save the resulting array of properties in a private field and use it further in serialization and deserialization operations. The cache is usually used by systems that run without stopping for a long time, and a serializer created once with a once initialized list of properties of a serializable type will exist for a long time. In case if this seems insufficient, it would be possible to additionally implement saving this list to the disk with reinitialization during the service restart, but this seems to be an unnecessary precaution.

However, when changing the composition of properties, their name, type or relative position, it is no longer possible to correctly deserialize the old version. However, since the idea described is intended to store data in a cache, rather than a permanent database, the easiest way to cope with the discrepancy is to disable the old byte representation and replace it with the newly serialized object. More complex approaches are possible, for example, using a separate AppDomain to load the old type version, deserialize an object, fill it with the properties of a new object, serialize it and save it with the same key; within the framework of this article, however, no attempts were made to do this.

In any case, in order to track changes in the object type, the serialization process should include the possibility of versioning. For example, before the bytes of the object itself the version of the assembly in which it is declared may be written:

 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); 

When serializing a set of different objects, for each of which the GetProperties () method is called only once and the result is stored in memory, it is necessary to somehow match the types of objects with the resulting property lists. To do this, you can either use the Type → PropertyInfo [] dictionary, or select specialized serializers for each serializable type using Generics. The second approach subjectively looks more convenient:

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

More productive approach


The next problem that arises when using Reflection in the context of this task is, again, performance: reflection has never been considered a fast mechanism. In such books on optimizing .Net applications like Sasha Goldshtein, Dima Zurbalev, Ido Flatow “Pro. Net Performance: Optimize Your C # Applications” and Ben Watson “Writing High Performance. NET Code” as one of the optimization techniques when working with Reflection and creating custom serializers is proposed to generate code, for example using the System.Reflection.Emit namespace tool. The idea with this approach is to create a code from the resulting property list, that is, a sequence of instructions that will alternately receive the value of each of the properties, write it to the byte stream, read, convert, set the value, and so on.

The ILGenerator class from the System.Reflection.Emit namespace contains a number of methods that allow you to create MSIL intermediate language instructions, which can then be compiled at run-time using the DynamicMethod class. In general terms, it looks like this:

 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); } } 

Of course, outside of the “common features”, the most complex and interesting part is the implementation of the EmitWriteNNNPropertyCode / EmitReadNNNN PropertyCode methods.

MSIL is a “high-level assembler” and the code on it is sometimes difficult to read, not just what to write, especially in a mediated way, by calling ILGenerator.Emit (OpCode) methods.

Here helps the trick, given in one of the aforementioned books: it is not necessary to write code on IL entirely from scratch. It is quite possible to create a “blank” in C #, create an assembly with it, disassemble into IL and, looking at the resulting reference implementation, generalize it in accordance with your needs.

Disassemblers, which allow to get IL code from .Net assembly, there is a large number of IL-code: ildasm, dotPeek, ILSpy, etc. It happened, however, that this project started under Microsoft OS was already written under Linux (good. NET Core allows), where the choice of disassemblers is not so great. However, tools are also available for this operating system, in particular monodis . You can get the text file and IL source code from the dll using monodis with the following command:

 monodis <  > --output=<   > 

Generic serialization of the simplest types


All actions performed by the generated serializer are similar to the operations of the original Reflection serializer, and repeating them through Emit is easier than it might seem at first glance. For example, a “stub” that gets the value of an int property and writes its bytes to a stream might look like this:

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

After assembly and decompilation, the corresponding IL code will contain the following instructions:

 .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 } 

And the code that repeats it using the Reflection.Emit namespace calls the following ILGenerator method sequence:

 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); 

It should be noted that the property value is obtained uniformly for all types using property.GetMethod. In the same way, the conversion of this value into an array of bytes is easily generalized: you just need to use a suitable argument of type MethodInfo. Thus, the same generator function can create code that serializes properties of different types, depending on the method of receiving an array of bytes transferred to it.
The System.BitConverter class contains several overloaded GetBytes methods for several built-in types, and for all these types, the serialization operation can be performed uniformly by selecting the desired MethodInfo option in BitConverterMethodsInfo.ChooseGetBytesOverloadByType (valueType).

Obtaining the MedodInfo object for the corresponding methods has to be done again via Reflection, and for faster access to them, it makes sense to save them in a static dictionary:

 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; } 

The above code allows you to generate code for several system types: bool, short, int, long, ushort, uint, ulong, double, float, char.

Decimal, guid and byte serialization


From the list of primitive types, the decimal type is knocked out, for which System.BitConverter does not have a built-in method for obtaining an array of bytes. Therefore, the transformation methods must be implemented independently:

 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); } 

For the Guid type, the overload of the BitConverter.GetBytes method is also absent, but getting its byte representation is trivial - using the Guid.ToByteArray method. To restore the value from the byte array, Guid has a constructor.

 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); } 

With byte, things are quite simple: the Stream class has special methods for writing and reading a single byte.

Serialization of date and time


The situation with the DateTime and DateTimeOffset types is a bit more complicated, since they are determined not only by the time value, but also by the Kind and Offset fields, respectively; these fields must be recorded / counted along with the time itself. Generating an IL code that saves the value of a DateTimeOffset variable, for example, looks like this:

 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); } 

Determining the type length in bytes


Deserialization of all the above types is symmetrical, but to read the corresponding byte array, you need to know its length. At run time, the property type is available as a value of the Type class, and the sizeof operation cannot be applied to it. The Marshal.SizeOf method comes to the rescue, which, however, does not apply to all types, and certainly does not return the number of bytes written by custom save implementations. For them, however, you can simply explicitly return the size:

 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> Serialization


When serializing Nullable <>, it is logical to first write a flag that determines whether the property contains null, and only if the value is not empty, write the value itself, using already implemented methods.

 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 type processing


Values ​​of type string do not have a fixed size and the number of bytes stored in them is not known in advance. However, the length of a particular string can be saved to the stream before writing the bytes that constitute it. When deserializing, you can first read an array of bytes containing an int with a string length, get a value of this length, and then read the corresponding byte number. In the case of a null string, you can write the value “-1” to be able to distinguish it from an empty string of length 0

Converting a string to a byte array / from it is easily done using the methods of Encoding.GetBytes / Encoding.GetString. For a change, here is a method for reading a string from a stream, not a record:

 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<>, , -, :


, 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() }; 


, «» Reflection- , , , - . EmitSerializer ( ). , :

 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 

Source


Github .

, , as is, . , , , « » .

.NET Core 2.0, Linux Ubuntu 16.04 LTS.

Source: https://habr.com/ru/post/358872/


All Articles