Rohdatenserialisierung von Structs

    • Allgemein
    • .NET (FX) 4.5–4.8

    Es gibt 4 Antworten in diesem Thema. Der letzte Beitrag () ist von jvbsl.

      Rohdatenserialisierung von Structs

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

      1. static byte[] GetDataMarshal<T>(T @struct) where T : struct
      2. {
      3. int size = Marshal.SizeOf(typeof(T));
      4. IntPtr buffer = Marshal.AllocHGlobal(size);
      5. try
      6. {
      7. Marshal.StructureToPtr<T>(@struct, buffer, false);
      8. byte[] data = new byte[size];
      9. Marshal.Copy(buffer, data, 0, size);
      10. return data;
      11. }
      12. finally
      13. {
      14. Marshal.FreeHGlobal(buffer);
      15. }
      16. }
      17. static T GetStructMarshal<T>(byte[] data) where T : struct
      18. {
      19. int size = Marshal.SizeOf(typeof(T));
      20. IntPtr buffer = Marshal.AllocHGlobal(size);
      21. try
      22. {
      23. Marshal.Copy(data, 0, buffer, size);
      24. T @struct = Marshal.PtrToStructure<T>(buffer, typeof(T));
      25. return @struct;
      26. }
      27. finally
      28. {
      29. Marshal.FreeHGlobal(buffer);
      30. }
      31. }

      VB.NET-Quellcode

      1. Private Shared Function GetDataMarshal(Of T As Structure)(struct As T) As Byte()
      2. Dim size As Integer = Marshal.SizeOf(GetType(T))
      3. Dim buffer As IntPtr = Marshal.AllocHGlobal(size)
      4. Try
      5. Marshal.StructureToPtr(Of T)(struct, buffer, False)
      6. Dim data As Byte() = New Byte(size - 1) {}
      7. Marshal.Copy(buffer, data, 0, size)
      8. Return data
      9. Finally
      10. Marshal.FreeHGlobal(buffer)
      11. End Try
      12. End Function
      13. Private Shared Function GetStructMarshal(Of T As Structure)(data As Byte()) As T
      14. Dim size As Integer = Marshal.SizeOf(GetType(T))
      15. Dim buffer As IntPtr = Marshal.AllocHGlobal(size)
      16. Try
      17. Marshal.Copy(data, 0, buffer, size)
      18. Dim struct As T = Marshal.PtrToStructure(Of T)(buffer, GetType(T))
      19. Return struct
      20. Finally
      21. Marshal.FreeHGlobal(buffer)
      22. End Try
      23. 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

      1. static unsafe byte[] GetDataGCHandle<T>(T @struct) where T : struct
      2. {
      3. byte[] data = new byte[Marshal.SizeOf(typeof(T))];
      4. GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
      5. try
      6. {
      7. Marshal.StructureToPtr<T>(@struct, handle.AddrOfPinnedObject(), false);
      8. return data;
      9. }
      10. finally
      11. {
      12. handle.Free();
      13. }
      14. }
      15. static unsafe T GetStructGCHandle<T>(byte[] data) where T : struct
      16. {
      17. GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
      18. try
      19. {
      20. T @struct = Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
      21. return @struct;
      22. }
      23. finally
      24. {
      25. handle.Free();
      26. }
      27. }

      VB.NET-Quellcode

      1. Private Shared Function GetDataGCHandle(Of T As Structure)(struct As T) As Byte()
      2. Dim data As Byte() = New Byte(Marshal.SizeOf(GetType(T)) - 1) {}
      3. Dim handle As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)
      4. Try
      5. Marshal.StructureToPtr(Of T)(struct, handle.AddrOfPinnedObject(), False)
      6. Return data
      7. Finally
      8. handle.Free()
      9. End Try
      10. End Function
      11. Private Shared Function GetStructGCHandle(Of T As Structure)(data As Byte()) As T
      12. Dim handle As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)
      13. Try
      14. Dim struct As T = Marshal.PtrToStructure(Of T)(handle.AddrOfPinnedObject())
      15. Return struct
      16. Finally
      17. handle.Free()
      18. End Try
      19. 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

      1. static unsafe byte[] GetDataUnsafe<T>(T @struct) where T : struct
      2. {
      3. byte[] data = new byte[Marshal.SizeOf(typeof(T))];
      4. fixed (byte* ptr = data)
      5. {
      6. Marshal.StructureToPtr<T>(@struct, (IntPtr)ptr, false);
      7. }
      8. return data;
      9. }
      10. static unsafe T GetStructUnsafe<T>(byte[] data) where T : struct
      11. {
      12. fixed (byte* ptr = data)
      13. {
      14. return Marshal.PtrToStructure<T>((IntPtr)ptr);
      15. }
      16. }

      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

      1. static unsafe byte[] GetDataMakeRef<T>(T @struct) where T : struct
      2. {
      3. int size = Marshal.SizeOf(typeof(T));
      4. byte[] data = new byte[size];
      5. TypedReference @ref = __makeref(@struct);
      6. Marshal.Copy(*((IntPtr*)&@ref), data, 0, size);
      7. return data;
      8. }
      9. static unsafe T GetStructMakeRef<T>(byte[] data) where T : struct
      10. {
      11. T @struct = default(T);
      12. TypedReference @ref = __makeref(@struct);
      13. Marshal.Copy(data, 0, *((IntPtr*)&@ref), Marshal.SizeOf(typeof(T)));
      14. return @struct;
      15. }

      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:
      MethodeDurchschnittliche Zeit
      Marshal0,89ns pro Struct
      GCHandle0,62ns pro Struct
      Unsafe0,46ns pro Struct
      __makeref0,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
      Ich glaube zum Deserealisieren gibt es noch ein schnellere kombination aus __makeref, Unsafe und dem Schlüsselwort __refvalue. Vielleicht kannst du das auch nochmal messen :) .

      C#-Quellcode

      1. static unsafe T GetStructMakeRef<T>(byte[] data) where T : struct
      2. {
      3. T @struct = default(T);
      4. TypedReference @ref = __makeref(@struct);
      5. fixed (byte* ptr = data)
      6. {
      7. *((IntPtr*)&@ref) = (IntPtr)ptr;
      8. return __refvalue(@ref, T);
      9. }
      10. }
      @Bluespide
      Stimmt, an refvalue hatte ich wohl in dem Moment gerade nicht gedacht - Danke für den Hinweis :)
      Ich werde das wenn ich Zeit habe vielleicht morgen testen und zurückmelden, ob das eine signifikante Verbesserung ergibt. Ich würde jedoch ehrlich gesagt nicht vermuten, dass es viel schneller ist - der Hauptvorteil der makeref-Variante ist ja das Umgehen von Marshal.StructToPtr, was einem gewaltigen Overhead und einen (intentionellen) Memoryleak erspart (aber die Methoden leider auch inkompatibel macht).

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nafets“ ()

      Mit der letzt Visual Studio Version 15.7 ist bei diesem Thema, im C# Bereich, dank des Punktes Unmanaged type constraint ein massiver performance Boost möglich, denn jetzt kann man Generische T Pointer sowie sizeof(T) im unmanaged Kontext verwenden. Dadurch sind folgende Methoden möglich, die man sonst nur im IL direkt schreiben konnte:

      C#-Quellcode

      1. static unsafe byte[] GetDataUnsafe<T>(T @struct) where T : unmanaged {
      2. byte[] data = new byte[sizeof(T)];
      3. fixed (byte* ptr = data) {
      4. *((T*)ptr) = @struct;
      5. }
      6. return data;
      7. }
      8. static unsafe T GetStructUnsafe<T>(byte[] data) where T : unmanaged {
      9. fixed (byte* ptr = data) {
      10. return *((T*)ptr);
      11. }
      12. }


      @nafets Vielleicht kannst du das ja auch noch Messen und deinem Test hinzufügen :)
      Sollte man auch mal damit vergleichen, ich denke fast die sollten noch mehr Performance rausholen:
      github.com/dotnet/corefx/blob/…ompilerServices.Unsafe.il
      Gibt es fertig als nuge package
      Ich wollte auch mal ne total überflüssige Signatur:
      ---Leer---