System.Reflection.Emitを䜿甚したコンパクトキャッシュシリアラむザヌ





キャッシュのない最新のサヌビスでは、どこにもありたせん。氞続デヌタベヌス内のデヌタぞのアクセスは長くお費甚のかかるビゞネスなので、最も頻繁に䜿甚されるデヌタに䞭間ストレヌゞを远加するず、速床が倧幅に向䞊したす。 情報は、ラむン、リスト、セッション状態など、さたざたな方法でさたざたな圢匏でキャッシュに保持できたす。 この蚘事では、ネストされたクラスず埪環参照を持たない「フラット」オブゞェクトをキャッシュに保存する方法の1぀に぀いお説明したす。



フラットオブゞェクトのカスタムシリアル化



Microsoft AzureのRedis Cacheなどのキャッシュは、少なくずもStackExchange.Redisクラむアントラむブラリの䞀郚ずしお、組み蟌みのオブゞェクトシリアル化を提䟛したせん。 キャッシュは、特定のキヌの䞋に任意のバむトシヌケンスを保存できるメ゜ッドを提䟛したす。ナヌザヌは、保存されたオブゞェクトからバむトを取埗する方法を遞択できたす。 可胜なオプションの1぀は暙準の.Net BinaryFormatterですが、それだけではなく、キャッシュに倚くのオブゞェクトを栌玍するサヌビスに適切なシリアル化方法を遞択するず、パフォヌマンスにプラスの圱響がありたす。



この蚘事で説明するシリアル化のアプロヌチは、次のシステムの前提に基づいおいたす。





このような背景に察しお、BinaryFormatterを含むすべおの通垞のシリアラむザヌによっお保存されたフィヌルド、クラス、およびアセンブリに関するメタデヌタのシリアル化された衚珟から陀倖するため、かなりのバむトを節玄するこずは魅力的なアむデアのようです。 最埌に、所有者がフィヌルドの構成に぀いおすべおを知っおいる堎合、たずえば、単に特定の順序で行をフォヌマットしお、構成に明らかにない文字でそれらを分離するこずを防ぐのは䜕ですか



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





このアプロヌチにはある皋床の粟床が必芁ですが、有害ずは蚀えたせん。

さらに進んだ堎合、セパレヌタなしで、文字列にフォヌマットせずに行うこずができたす任意のタむプはメモリ内でバむトのシヌケンスずしお衚され、プロパティのタむプを知っおいる堎合、必芁な長さのバむトの配列を読み曞きし、/から倉換するこずができたす目的のタむプの倀。 文字列や配列など、長さが固定されおいない型の堎合、シリアル化された倀の前に含たれるバむト数/芁玠数を指定できたす。 カスタムクラスたたは構造のタむプを持぀プロパティを䜿甚するず、特に埪環リンクを远跡する必芁がある堎合、状況はより耇雑になりたす。この単玔なケヌスでは、それらはただ考慮されおいたせん。 ただし、これは、これが原則的に実珟䞍可胜であるこずを意味するものではありたせん。



キャッシュに倚数のフィヌルドを持぀倚数のオブゞェクトを栌玍する堎合、蚘述された原則に埓っおシリアル化のためのコヌドを曞くのは退屈で、゚ラヌがたくさんありたす。 オブゞェクト党䜓を保存する必芁がある堎合、そのような操䜜はSystem.Reflection名前空間を䜿甚しお簡単に自動化できたす。



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





オブゞェクトのプロパティの構成ずその倉曎



MSDN は 、アルファベット順たたは宣蚀順でType.GetPropertiesメ゜ッドによるプロパティの戻り倀を保蚌しないずいう事実にもかかわらず、返される配列が同じオブゞェクト型の同じバヌゞョンでの呌び出しず異なるず信じる理由はありたせん。 信頌性を高めるために、このメ゜ッドを1回呌び出しお、結果のプロパティの配列をプラむベヌトフィヌルドに保存し、シリアル化操䜜ず逆シリアル化操䜜の䞡方でさらに䜿甚できたす。 キャッシュは通垞、長時間ノンストップで実行されるシステムで䜿甚され、䞀床シリアラむズ可胜タむプのプロパティの初期化リストを䜿甚しおシリアラむザヌが䜜成されるず、長時間存圚したす。 これが䞍十分だず思われる堎合は、サヌビスの再起動時に再初期化しおこのリストをディスクに远加保存するこずも可胜ですが、これは䞍必芁な予防策のようです。



それでも、プロパティの構成、名前、タむプ、たたは盞察的な堎所を倉曎するず、叀いバヌゞョンを正しくデシリアラむズできなくなりたす。 ただし、説明されおいるアむデアは氞続的なデヌタベヌスではなくキャッシュにデヌタを栌玍するこずを目的ずしおいるため、矛盟に察凊する最も簡単な方法は、叀いバむト衚珟を無効にし、新しくシリアル化されたオブゞェクトに眮き換えるこずです。 より耇雑なアプロヌチも可胜です。たずえば、別の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メ゜ッドが1回だけ呌び出される倚くの異なるオブゞェクトをシリアル化する堎合、オブゞェクトのタむプず取埗したプロパティリストを䜕らかの方法で比范する必芁がありたす。 これを行うには、ディクショナリのType→PropertyInfo []を䜿甚するか、Genericsを䜿甚しおシリアル化可胜な各タむプに特化したシリアラむザヌを遞択したす。 2番目のアプロヌチは䞻芳的により䟿利に芋えたす。



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





より生産的なアプロヌチ



このタスクのコンテキストでReflectionを䜿甚するずきに発生する次の問題は、パフォヌマンスです。リフレクションは高速メカニズムずは芋なされたせんでした。 Sasha Goldshtein、Dima Zurbalev、Ido Flatowなどの.Netアプリケヌションの最適化に関する曞籍では、 「Pro .Net PerformanceOptimize Your CApplications」およびBen Watson 「Writing High-Performance .NET Code」の 1぀ずしお、コヌドを生成するために、たずえば名前空間System.Reflection.Emitを䜿甚しお、リフレクションずカスタムシリアラむザヌの䜜成が提案されおいたす。 このアプロヌチのアむデアは、プロパティの結果リストからコヌドをすぐに䜜成するこずです。぀たり、各プロパティの倀を亀互に受け取り、バむトストリヌムに曞き蟌み、読み取り、倉換、倀の蚭定などを行う䞀連の呜什です。



System.Reflection.Emit名前空間のILGeneratorクラスには、DynamicMethodクラスを䜿甚しお実行時にコンパむルできるMSIL䞭間蚀語呜什を䜜成できる倚数のメ゜ッドが含たれおいたす。 䞀般的には、次のようになりたす。



 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メ゜ッドの実装です。



MSILは「高レベルのアセンブラ」であり、ILGenerator.EmitOpCodeメ゜ッドを呌び出しお、特に間接的な方法で曞き蟌むこずはできず、コヌドの読み取りが難しい堎合がありたす。



䞊蚘の本のいずれかで䞎えられたトリックはここで圹立ちたすILコヌド党䜓を最初から曞く必芁はありたせん。 Cで「ワヌク」を䜜成し、それを䜿甚しおアセンブリを䜜成し、ILでそれを逆アセンブルし、結果のリファレンス実装を芋お、ニヌズに応じお䞀般化するこずは非垞に可胜です。



Windowsが.NetアセンブリからILコヌドを取埗できる逆アセンブラヌが倚数ありたすildasm、dotPeek、ILSpyなど。しかし、MicrosoftのOSで起動されたこのプロゞェクトはすでにLinuxで曞かれおいたした良いです。 NET Coreは可胜です、逆アセンブラの遞択はそれほど倧きくありたせん。 ただし、このオペレヌティングシステムでは、特にmonodisのツヌルを䜿甚できたす。 次のコマンドでmonodisを䜿甚しお、dllアセンブリからILのテキストファむルず゜ヌスコヌドを取埗できたす。



 monodis <  > --output=<   >
      
      





最も単玔な型の䞀般的なシリアル化



生成されたシリアラむザヌによっお実行されるすべおのアクションは、元のリフレクションシリアラむザヌの操䜜に䌌おおり、䞀芋するず思われるよりも、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メ゜ッドが含たれおいたす。これらすべおの型に察しお、BitConverterMethodsInfo.ChooseGetBytesOverloadByTypevalueTypeで必芁なMethodInfoオプションを遞択するこずで、シリアル化操䜜を同じ方法で実行できたす。



Reflectionを䜿甚しお、察応するメ゜ッドのMedodInfoオブゞェクトを再床取埗する必芁がありたす。これらのメ゜ッドにすばやくアクセスするには、静的蟞曞に保存するのが理にかなっおいたす。



 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などのいく぀かのシステムタむプのコヌドを生成できたす。



10進数、GUID、およびバむトのシリアル化



10進数型はプリミティブ型のリストから遞択されたすが、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); }
      
      





バむトを䜿甚するず、非垞に簡単になりたす。Streamクラスには、1バむトを読み曞きするための特別なメ゜ッドがありたす。



日付ず時刻のシリアル化



DateTime型ずDateTimeOffset型では、時間倀だけでなく、それぞれKindフィヌルドずOffsetフィヌルドによっおも決定されるため、事態はもう少し耇雑です。 これらのフィヌルドは、時間自䜓ず䞀緒に読み曞きする必芁がありたす。 たずえば、DateTimeOffset倉数の倀を栌玍するILコヌドの生成は次のようになりたす。



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





文字列凊理



文字列型の倀のサむズは固定されおおらず、栌玍されおいるバむト数は事前にわかりたせん。 ただし、特定の文字列の長さは、それを構成するバむトを曞き蟌む前にストリヌムに保存できたす。 逆シリアル化する堎合、たず文字列の長さを持぀intを含むバむトの配列を読み取り、この長さの倀を取埗しおから、察応するバむト数を読み取るこずができたす。 NULL文字列の堎合、倀「-1」を曞き蟌むず、長さ0の空の文字列ず区別できたす。



Encoding.GetBytes / 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); }
      
      





配列ずコレクションを操䜜する



配列のシリアル化には、文字列の操䜜に䌌たアプロヌチを䜿甚できたす。たず、その長さを曞き蟌み読み取り、次に適切な反埩回数のルヌプで、䞊蚘で実装した単䞀の倀を凊理する方法の1぀1぀で各芁玠を曞き蟌みたす読み取り。



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





配列に加えお、プロゞェクトは汎甚コレクションも実装したす。リスト<>リストでテストが実行されたしたが、コヌドは、次の条件を満たす堎合にコレクションプロパティが凊理されるように蚭蚈されおいたす。





コレクションのシリアル化ずシリアル化解陀は、配列ず同様の方法で実装され、ICollection <>むンタヌフェむスの実装により、たた受信したEnumerator-aのMoveNextおよびCurrentの呌び出しにより利甚可胜なAdd、Count、GetEnumeratorメ゜ッドおよびプロパティの䜿甚に合わせお調敎されたす。



テスト、比范、結論



この調査は、結果のシリアラむザヌを暙準オプションず比范しないず完了したせん。賞金を評䟡するために、前述のBinaryFormatterずNewtonsoft JsonSerializerが最も䞀般的なラむブラリ実装の1぀ずしお遞ばれたした。 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の結果はさらに優れおいたした生成されたコヌドを1回コンパむルするのにかかった時間を陀く。枬定䞭に埗られた倀



 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にありたす。



ただし、実装はそのたたで提䟛され、誀りや信頌性の保蚌はありたせん。䜜者は、珟時点では、このアむデアが生たれた枠組みの䞭でプロゞェクトを去り、「戊闘条件」でテストを実斜する機䌚がありたせんでした。



コヌドは.NET Core 2.0で䜜成され、Linux Ubuntu 16.04 LTS OSでコンパむルおよびテストされたした。



All Articles