Inspiriert von diesem Thread möchte ich hier die verschiedenen, mir bekannten Standard-Möglichkeiten für die Rohdatenserialisierung von Structs vorstellen und vergleichen. Ich habe dafür 3 verschiedene, oft genutzte Varianten gewählt, welche mit den Methoden
Hier werden die Daten mit einem über die Methode
Um nicht ständig die Daten in und aus dem nicht gemanagten Arbeitsspeicher kopieren zu müssen (-> "Marshal"-Methode), kann man auch einen gepinnten
Anstatt den langsamen
Die Konversion von Struct zu Rohdaten und zurück findet jedoch trotzdem über die Methoden
Diese Variante ist darüber hinaus nicht in VB.NET umsetzbar, da sie auf die C#-exklusiven Pointer zurückgreift.
Diese Variante ist besonders interessant, da sie einerseits eine
Da viele
Diese TypedReferences stellen jedoch nicht direkt einen Pointer auf die Rohdaten zur Verfügung, weshalb man etwas tricksen muss: Das erste Feld in der Struktur enthält nämlich den gewünschten Pointer - dieses ist jedoch privat. Um nun an den Wert dieses Feldes heranzukommen könnte man Reflection nutzen, diese ist jedoch langsam. Stattdessen kann man einen Pointer auf das TypedReference-Struct erstellen und durch einen Cast dieses Pointers zu einem
Da wir nun den direkten Pointer auf die Rohdaten des Structs haben, können wir auf diese direkt zugreifen und mit
Diese Variante ist wie die mit Pointern soweit ich weiß nicht in VB.NET umsetzbar, da sie auf das C#-exklusive Keyword
Aber wie siehts denn nun mit der Geschwindigkeit aus? Ich habe dafür jeden Algorithmus 3 Mal 5 Millionen Test-Strukturen serialisieren und deserialisieren lassen und die durchschnittliche benötigte Zeit gemessen (Test-Code). Die Ergebnisse habe ich in einer Tabelle zusammengefasst:
Hier zeigt sich klar der Vorteil der TypedReference, welche nicht auf die
Hoffentlich sind diese Informationen und meine Implementierungen für euch hilfreich, sodass ihr wenn ihr Rohdaten - beispielsweise für Netzwerkkommunikation - serialisieren wollt, einfach auf diesen Post zurückgreifen könnt.
Vielen Dank fürs Lesen,
Stefan
Marshal.PtrToStructure()
und Marshal.StructureToPtr()
arbeiten und stelle noch eine eigene vor, welche mit TypedReferences arbeitet, um dem Overhead dieser beiden Methoden zu entgehen. Ich habe mich auch auf generische Methoden beschränkt, da typenspezifische Methoden auf andere Weisen performant implementierbar sind und habe ebenfalls darauf verzichtet, Implementationen in MSIL dazuzunehmen, da die meisten Leute hier damit sowieso kaum etwas anfangen können. Hier erstmal die Codes der einzelnen Methoden, jeweils mit einer kleinen Erklärung:C#-Quellcode
- static byte[] GetDataMarshal<T>(T @struct) where T : struct
- {
- int size = Marshal.SizeOf(typeof(T));
- IntPtr buffer = Marshal.AllocHGlobal(size);
- try
- {
- Marshal.StructureToPtr<T>(@struct, buffer, false);
- byte[] data = new byte[size];
- Marshal.Copy(buffer, data, 0, size);
- return data;
- }
- finally
- {
- Marshal.FreeHGlobal(buffer);
- }
- }
- static T GetStructMarshal<T>(byte[] data) where T : struct
- {
- int size = Marshal.SizeOf(typeof(T));
- IntPtr buffer = Marshal.AllocHGlobal(size);
- try
- {
- Marshal.Copy(data, 0, buffer, size);
- T @struct = Marshal.PtrToStructure<T>(buffer, typeof(T));
- return @struct;
- }
- finally
- {
- Marshal.FreeHGlobal(buffer);
- }
- }
VB.NET-Quellcode
- Private Shared Function GetDataMarshal(Of T As Structure)(struct As T) As Byte()
- Dim size As Integer = Marshal.SizeOf(GetType(T))
- Dim buffer As IntPtr = Marshal.AllocHGlobal(size)
- Try
- Marshal.StructureToPtr(Of T)(struct, buffer, False)
- Dim data As Byte() = New Byte(size - 1) {}
- Marshal.Copy(buffer, data, 0, size)
- Return data
- Finally
- Marshal.FreeHGlobal(buffer)
- End Try
- End Function
- Private Shared Function GetStructMarshal(Of T As Structure)(data As Byte()) As T
- Dim size As Integer = Marshal.SizeOf(GetType(T))
- Dim buffer As IntPtr = Marshal.AllocHGlobal(size)
- Try
- Marshal.Copy(data, 0, buffer, size)
- Dim struct As T = Marshal.PtrToStructure(Of T)(buffer, GetType(T))
- Return struct
- Finally
- Marshal.FreeHGlobal(buffer)
- End Try
- End Function
Hier werden die Daten mit einem über die Methode
Marshal.AllocHGlobal
allozierten Handle im nicht gemanagten Arbeitsspeicher gespeichert und davon gelesen. Um die Daten zwischen dem gemanagten Array und dem nicht gemanagten Arbeitsspeicher hin und her zu bewegen wird die Methode Marshal.Copy
verwendet. Die Konversion von Struct zu Rohdaten und zurück findet über die Methoden Marshal.StructureToPtr()
und Marshal.PtrToStructure()
statt.C#-Quellcode
- static unsafe byte[] GetDataGCHandle<T>(T @struct) where T : struct
- {
- byte[] data = new byte[Marshal.SizeOf(typeof(T))];
- GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
- try
- {
- Marshal.StructureToPtr<T>(@struct, handle.AddrOfPinnedObject(), false);
- return data;
- }
- finally
- {
- handle.Free();
- }
- }
- static unsafe T GetStructGCHandle<T>(byte[] data) where T : struct
- {
- GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
- try
- {
- T @struct = Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
- return @struct;
- }
- finally
- {
- handle.Free();
- }
- }
VB.NET-Quellcode
- Private Shared Function GetDataGCHandle(Of T As Structure)(struct As T) As Byte()
- Dim data As Byte() = New Byte(Marshal.SizeOf(GetType(T)) - 1) {}
- Dim handle As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)
- Try
- Marshal.StructureToPtr(Of T)(struct, handle.AddrOfPinnedObject(), False)
- Return data
- Finally
- handle.Free()
- End Try
- End Function
- Private Shared Function GetStructGCHandle(Of T As Structure)(data As Byte()) As T
- Dim handle As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)
- Try
- Dim struct As T = Marshal.PtrToStructure(Of T)(handle.AddrOfPinnedObject())
- Return struct
- Finally
- handle.Free()
- End Try
- End Function
Um nicht ständig die Daten in und aus dem nicht gemanagten Arbeitsspeicher kopieren zu müssen (-> "Marshal"-Methode), kann man auch einen gepinnten
GCHandle
erstellen, welcher auf das gemanagte Datenarray zeigt - dadurch lässt sich direkt auf die Rohdaten zugreifen. Die Konversion von Struct zu Rohdaten und zurück findet wie bei der "Marshal"-Methode über die Methoden Marshal.StructureToPtr()
und Marshal.PtrToStructure()
statt.C#-Quellcode
- static unsafe byte[] GetDataUnsafe<T>(T @struct) where T : struct
- {
- byte[] data = new byte[Marshal.SizeOf(typeof(T))];
- fixed (byte* ptr = data)
- {
- Marshal.StructureToPtr<T>(@struct, (IntPtr)ptr, false);
- }
- return data;
- }
- static unsafe T GetStructUnsafe<T>(byte[] data) where T : struct
- {
- fixed (byte* ptr = data)
- {
- return Marshal.PtrToStructure<T>((IntPtr)ptr);
- }
- }
Anstatt den langsamen
GCHandle
zu verwenden, kann man das gleiche auch mit einem C#-Pointer machen, welcher über das fixed
-Statement erstellt wird.Die Konversion von Struct zu Rohdaten und zurück findet jedoch trotzdem über die Methoden
Marshal.StructureToPtr()
und Marshal.PtrToStructure()
statt.Diese Variante ist darüber hinaus nicht in VB.NET umsetzbar, da sie auf die C#-exklusiven Pointer zurückgreift.
C#-Quellcode
- static unsafe byte[] GetDataMakeRef<T>(T @struct) where T : struct
- {
- int size = Marshal.SizeOf(typeof(T));
- byte[] data = new byte[size];
- TypedReference @ref = __makeref(@struct);
- Marshal.Copy(*((IntPtr*)&@ref), data, 0, size);
- return data;
- }
- static unsafe T GetStructMakeRef<T>(byte[] data) where T : struct
- {
- T @struct = default(T);
- TypedReference @ref = __makeref(@struct);
- Marshal.Copy(data, 0, *((IntPtr*)&@ref), Marshal.SizeOf(typeof(T)));
- return @struct;
- }
Diese Variante ist besonders interessant, da sie einerseits eine
TypedReference
nutzt, welche über die Methode __makeref()
erstellt wird, und ohne Marshal.StructureToPtr()
und Marshal.PtrToStructure()
auskommt.Da viele
__makeref
und TypedReferences wahrscheinlich nicht kennen, hier eine kurze Erklärung: __makeref
ist ein C#-Keyword, welches TypedReferences auf beliebige Objekte erstellen kann. Dadurch ist es indirekt möglich, generische Pointer zu nutzen, obwohl diese eigentlich nicht vorgesehen sind.Diese TypedReferences stellen jedoch nicht direkt einen Pointer auf die Rohdaten zur Verfügung, weshalb man etwas tricksen muss: Das erste Feld in der Struktur enthält nämlich den gewünschten Pointer - dieses ist jedoch privat. Um nun an den Wert dieses Feldes heranzukommen könnte man Reflection nutzen, diese ist jedoch langsam. Stattdessen kann man einen Pointer auf das TypedReference-Struct erstellen und durch einen Cast dieses Pointers zu einem
IntPtr*
- einem Pointer auf einen IntPtr, einen Pointer auf den Pointer erhalten, welcher wiederum auf die Rohdaten zeigt. Dieser muss dann nur noch dereferenziert werden und dadurch ergibt sich das Konstrukt *((IntPtr*)&@ref)
.Da wir nun den direkten Pointer auf die Rohdaten des Structs haben, können wir auf diese direkt zugreifen und mit
Marshal.Copy()
von einem gemanagten Bytearray befüllen oder in ein gemanagtes Bytearray kopieren, ohne die langsameren Marshal.StructureToPtr()
- und Marshal.PtrToStructure()
-Methoden nutzen zu müssen.Diese Variante ist wie die mit Pointern soweit ich weiß nicht in VB.NET umsetzbar, da sie auf das C#-exklusive Keyword
__makeref
zurückgreift.Aber wie siehts denn nun mit der Geschwindigkeit aus? Ich habe dafür jeden Algorithmus 3 Mal 5 Millionen Test-Strukturen serialisieren und deserialisieren lassen und die durchschnittliche benötigte Zeit gemessen (Test-Code). Die Ergebnisse habe ich in einer Tabelle zusammengefasst:
Methode | Durchschnittliche Zeit |
Marshal | 0,89ns pro Struct |
GCHandle | 0,62ns pro Struct |
Unsafe | 0,46ns pro Struct |
__makeref | 0,17ns pro Struct |
Hier zeigt sich klar der Vorteil der TypedReference, welche nicht auf die
Marshal.StructureToPtr()
- und Marshal.PtrToStructure()
-Methoden zurückgreifen muss, welche offensichtlich einigen Overhead erzeugen. Dadurch ist sie auch der konventionellen Unsafe-Methode überlegen. Bei den "nicht-unsafen" Methoden ist der GCHandle
den Marshal
-Methoden ebenfalls klar überlegen, weshalb man bei Verwendung von VB.NET oder Verzicht auf unsafe-Code eher auf diese Variante setzen sollte.Hoffentlich sind diese Informationen und meine Implementierungen für euch hilfreich, sodass ihr wenn ihr Rohdaten - beispielsweise für Netzwerkkommunikation - serialisieren wollt, einfach auf diesen Post zurückgreifen könnt.
Vielen Dank fürs Lesen,
Stefan