Performances C # – Utilisation de pointeurs non sécurisés au lieu d’IntPtr et de Marshal

Question

Je porte une application C dans C #. L’application C appelle beaucoup de fonctions à partir d’une DLL tierce, j’ai donc écrit des wrappers P / Invoke pour ces fonctions en C #. Certaines de ces fonctions C allouent des données que je dois utiliser dans l’application C #, j’ai donc utilisé IntPtr , Marshal.PtrToStructure et Marshal.Copy pour copier les données natives (tableaux et structures) dans des variables gérées.

Malheureusement, l’application C # s’est révélée beaucoup plus lente que la version C. Une parsing de performance rapide a montré que la copie de données basée sur le marshaling mentionné ci-dessus est le goulot d’étranglement. J’envisage d’accélérer le code C # en le réécrivant pour utiliser des pointeurs à la place. Comme je n’ai pas d’expérience avec le code dangereux et les pointeurs en C #, j’ai besoin d’un avis d’expert sur les questions suivantes:

  1. Quels sont les inconvénients de l’utilisation de code et de pointeurs non IntPtr au lieu d’ IntPtr et de Marshal ing? Par exemple, est-ce plus dangereux (jeu de mots) de quelque manière que ce soit? Les gens semblent préférer le marshaling, mais je ne sais pas pourquoi.
  2. L’utilisation de pointeurs pour P / Invoquer est-elle vraiment plus rapide que l’utilisation du marshaling? Combien d’accélération peut-on attendre approximativement? Je n’ai pas pu trouver de tests de référence pour cela.

Exemple de code

Pour rendre la situation plus claire, j’ai assemblé un petit exemple de code (le code réel est beaucoup plus complexe). J’espère que cet exemple montre ce que je veux dire quand je parle de “code non sécurisé et de pointeurs” contre “IntPtr et Marshal”.

Bibliothèque C (DLL)

Mylib.h

 #ifndef _MY_LIB_H_ #define _MY_LIB_H_ struct MyData { int length; unsigned char* bytes; }; __declspec(dllexport) void CreateMyData(struct MyData** myData, int length); __declspec(dllexport) void DestroyMyData(struct MyData* myData); #endif // _MY_LIB_H_ 

MyLib.c

 #include  #include "MyLib.h" void CreateMyData(struct MyData** myData, int length) { int i; *myData = (struct MyData*)malloc(sizeof(struct MyData)); if (*myData != NULL) { (*myData)->length = length; (*myData)->bytes = (unsigned char*)malloc(length * sizeof(char)); if ((*myData)->bytes != NULL) for (i = 0; i bytes[i] = (unsigned char)(i % 256); } } void DestroyMyData(struct MyData* myData) { if (myData != NULL) { if (myData->bytes != NULL) free(myData->bytes); free(myData); } } 

C application

Principal c

 #include  #include "MyLib.h" void main() { struct MyData* myData = NULL; int length = 100 * 1024 * 1024; printf("=== C++ test ===\n"); CreateMyData(&myData, length); if (myData != NULL) { printf("Length: %d\n", myData->length); if (myData->bytes != NULL) printf("First: %d, last: %d\n", myData->bytes[0], myData->bytes[myData->length - 1]); else printf("myData->bytes is NULL"); } else printf("myData is NULL\n"); DestroyMyData(myData); getchar(); } 

Application C #, qui utilise IntPtr et Marshal

Program.cs

 using System; using System.Runtime.InteropServices; public static class Program { [StructLayout(LayoutKind.Sequential)] private struct MyData { public int Length; public IntPtr Bytes; } [DllImport("MyLib.dll")] private static extern void CreateMyData(out IntPtr myData, int length); [DllImport("MyLib.dll")] private static extern void DestroyMyData(IntPtr myData); public static void Main() { Console.WriteLine("=== C# test, using IntPtr and Marshal ==="); int length = 100 * 1024 * 1024; IntPtr myData1; CreateMyData(out myData1, length); if (myData1 != IntPtr.Zero) { MyData myData2 = (MyData)Marshal.PtrToStructure(myData1, typeof(MyData)); Console.WriteLine("Length: {0}", myData2.Length); if (myData2.Bytes != IntPtr.Zero) { byte[] bytes = new byte[myData2.Length]; Marshal.Copy(myData2.Bytes, bytes, 0, myData2.Length); Console.WriteLine("First: {0}, last: {1}", bytes[0], bytes[myData2.Length - 1]); } else Console.WriteLine("myData.Bytes is IntPtr.Zero"); } else Console.WriteLine("myData is IntPtr.Zero"); DestroyMyData(myData1); Console.ReadKey(true); } } 

Application C #, qui utilise du code et des pointeurs unsafe

Program.cs

 using System; using System.Runtime.InteropServices; public static class Program { [StructLayout(LayoutKind.Sequential)] private unsafe struct MyData { public int Length; public byte* Bytes; } [DllImport("MyLib.dll")] private unsafe static extern void CreateMyData(out MyData* myData, int length); [DllImport("MyLib.dll")] private unsafe static extern void DestroyMyData(MyData* myData); public unsafe static void Main() { Console.WriteLine("=== C# test, using unsafe code ==="); int length = 100 * 1024 * 1024; MyData* myData; CreateMyData(out myData, length); if (myData != null) { Console.WriteLine("Length: {0}", myData->Length); if (myData->Bytes != null) Console.WriteLine("First: {0}, last: {1}", myData->Bytes[0], myData->Bytes[myData->Length - 1]); else Console.WriteLine("myData.Bytes is null"); } else Console.WriteLine("myData is null"); DestroyMyData(myData); Console.ReadKey(true); } } 

C’est un peu vieux thread, mais j’ai récemment fait des tests de performance excessifs avec marshaling en C #. J’ai besoin de démasquer un grand nombre de données depuis un port série sur plusieurs jours. Il était important pour moi de ne pas avoir de memory leaks (car la plus petite fuite serait importante après quelques millions d’appels) et j’ai également effectué beaucoup de tests de performances statistiques (temps utilisé) avec de très grosses structures (> 10kb) sake of it (un non, vous ne devriez jamais avoir une structure 10kb :-))

J’ai testé les trois stratégies de démasquage suivantes (j’ai également testé le regroupement). Dans presque tous les cas, le premier (MarshalMatters) a surpassé les deux autres. Marshal.Copy était de loin le plus lent, les deux autres étaient pour la plupart très proches dans la course.

L’utilisation d’un code non sécurisé peut poser un risque de sécurité important.

Premier:

 public class MarshalMatters { public static T ReadUsingMarshalUnsafe(byte[] data) where T : struct { unsafe { fixed (byte* p = &data[0]) { return (T)Marshal.PtrToStructure(new IntPtr(p), typeof(T)); } } } public unsafe static byte[] WriteUsingMarshalUnsafe(selectedT structure) where selectedT : struct { byte[] byteArray = new byte[Marshal.SizeOf(structure)]; fixed (byte* byteArrayPtr = byteArray) { Marshal.StructureToPtr(structure, (IntPtr)byteArrayPtr, true); } return byteArray; } } 

Seconde:

 public class Adam_Robinson { private static T BytesToStruct(byte[] rawData) where T : struct { T result = default(T); GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned); try { IntPtr rawDataPtr = handle.AddrOfPinnedObject(); result = (T)Marshal.PtrToStructure(rawDataPtr, typeof(T)); } finally { handle.Free(); } return result; } ///  /// no Copy. no unsafe. Gets a GCHandle to the memory via Alloc ///  ///  ///  ///  public static byte[] StructToBytes(T structure) where T : struct { int size = Marshal.SizeOf(structure); byte[] rawData = new byte[size]; GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned); try { IntPtr rawDataPtr = handle.AddrOfPinnedObject(); Marshal.StructureToPtr(structure, rawDataPtr, false); } finally { handle.Free(); } return rawData; } } 

Troisième:

 ///  /// http://stackoverflow.com/questions/2623761/marshal-ptrtostructure-and-back-again-and-generic-solution-for-endianness-swap ///  public class DanB { ///  /// uses Marshal.Copy! Not run in unsafe. Uses AllocHGlobal to get new memory and copies. ///  public static byte[] GetBytes(T structure) where T : struct { var size = Marshal.SizeOf(structure); //or Marshal.SizeOf(); in .net 4.5.1 byte[] rawData = new byte[size]; IntPtr ptr = Marshal.AllocHGlobal(size); Marshal.StructureToPtr(structure, ptr, true); Marshal.Copy(ptr, rawData, 0, size); Marshal.FreeHGlobal(ptr); return rawData; } public static T FromBytes(byte[] bytes) where T : struct { var structure = new T(); int size = Marshal.SizeOf(structure); //or Marshal.SizeOf(); in .net 4.5.1 IntPtr ptr = Marshal.AllocHGlobal(size); Marshal.Copy(bytes, 0, ptr, size); structure = (T)Marshal.PtrToStructure(ptr, structure.GetType()); Marshal.FreeHGlobal(ptr); return structure; } } 

Les considérations d’interopérabilité expliquent pourquoi et quand Marshaling est requirejs et à quel coût. Citation:

  1. Le marshaling se produit lorsqu’un appelant et un appelé ne peuvent pas fonctionner sur la même instance de données.
  2. le marshaling répété peut affecter négativement les performances de votre application.

Par conséquent, répondre à votre question si

… utiliser des pointeurs pour P / Invoke vraiment plus rapide que d’utiliser le marshaling …

Tout d’abord, posez-vous une question si le code géré peut fonctionner sur l’instance de valeur de retour de méthode non gérée. Si la réponse est oui, Marshaling et le coût de performance associé ne sont pas requirejs. Le gain de temps approximatif serait la fonction O (n)n correspond à la taille de l’instance marshallée. De plus, ne pas conserver les blocs de données gérés et non gérés en mémoire en même temps pendant la durée de la méthode (dans l’exemple “IntPtr et Marshal”) élimine la surcharge et la pression de la mémoire.

Quels sont les inconvénients de l’utilisation de code et de pointeurs non sécurisés …

L’inconvénient est le risque associé à l’access à la mémoire directement via des pointeurs. Il n’y a rien de moins sûr que d’utiliser des pointeurs en C ou en C ++. Utilisez-le si nécessaire et fait sens. Plus de détails sont ici .

Il y a un problème de “sécurité” avec les exemples présentés: la libération de la mémoire non gérée allouée n’est pas garantie après les erreurs de code géré. La meilleure pratique est de

 CreateMyData(out myData1, length); if(myData1!=IntPtr.Zero) { try { // -> use myData1 ... // <- } finally { DestroyMyData(myData1); } } 

Deux réponses,

  1. Un code non sécurisé signifie qu’il n’est pas géré par le CLR. Vous devez prendre soin des ressources qu’il utilise.

  2. Vous ne pouvez pas mettre à l’échelle les performances car il y a tellement de facteurs qui ont une incidence. Mais utiliser des pointeurs sera beaucoup plus rapide.

Je voulais juste append mon expérience à cet ancien sujet: Nous avons utilisé Marshaling dans un logiciel d’enregistrement sonore – nous avons reçu des données sonores en temps réel de la console de mixage dans des tampons natifs et les avons rassemblés en octets []. C’était un véritable tueur de performance. Nous avons été obligés de passer à des structures non sûres comme seul moyen de terminer la tâche.

Si vous ne disposez pas de structures natives volumineuses et que cela ne vous dérange pas que toutes les données soient remplies deux fois, Marshaling est une approche plus élégante et beaucoup plus sûre.

Étant donné que vous avez déclaré que votre code appelle la DLL tierce, je pense que le code dangereux est plus adapté dans votre scénario. Vous avez rencontré une situation particulière de permutation de tableaux de longueur variable dans une struct ; Je sais, je sais que ce genre d’utilisation se produit tout le temps, mais ce n’est pas toujours le cas après tout. Vous voudrez peut-être examiner quelques questions à ce sujet, par exemple:

Comment regrouper une structure contenant un tableau de taille variable en C #?

Si .. je dis si .. vous pouvez modifier les bibliothèques tierces un peu pour ce cas particulier, alors vous pourriez envisager l’utilisation suivante:

 using System.Runtime.InteropServices; public static class Program { /* [StructLayout(LayoutKind.Sequential)] private struct MyData { public int Length; public byte[] Bytes; } */ [DllImport("MyLib.dll")] // __declspec(dllexport) void WINAPI CreateMyDataAlt(BYTE bytes[], int length); private static extern void CreateMyDataAlt(byte[] myData, ref int length); /* [DllImport("MyLib.dll")] private static extern void DestroyMyData(byte[] myData); */ public static void Main() { Console.WriteLine("=== C# test, using IntPtr and Marshal ==="); int length = 100*1024*1024; var myData1 = new byte[length]; CreateMyDataAlt(myData1, ref length); if(0!=length) { // MyData myData2 = (MyData)Marshal.PtrToStructure(myData1, typeof(MyData)); Console.WriteLine("Length: {0}", length); /* if(myData2.Bytes!=IntPtr.Zero) { byte[] bytes = new byte[myData2.Length]; Marshal.Copy(myData2.Bytes, bytes, 0, myData2.Length); */ Console.WriteLine("First: {0}, last: {1}", myData1[0], myData1[length-1]); /* } else { Console.WriteLine("myData.Bytes is IntPtr.Zero"); } */ } else { Console.WriteLine("myData is empty"); } // DestroyMyData(myData1); Console.ReadKey(true); } } 

Comme vous pouvez le voir, une grande partie de votre code de marshalling original est commenté, et a déclaré CreateMyDataAlt(byte[], ref int) pour une fonction non gérée externe CreateMyDataAlt(BYTE [], int) . Une partie de la copie de données et de la vérification du pointeur devient inutile, ce qui signifie que le code peut être encore plus simple et peut-être plus rapide.

Alors, quelle est la différence avec la modification? Le tableau d’octets est maintenant dirigé directement sans mise en forme dans une struct et transmis au côté non géré. Vous n’affectez pas la mémoire dans le code non géré, mais vous lui remplissez simplement les données (détails d’implémentation omis); et après l’appel, les données nécessaires sont fournies au côté géré. Si vous souhaitez présenter que les données ne sont pas remplies et ne doivent pas être utilisées, vous pouvez simplement définir la length à zéro pour indiquer le côté géré. Comme le tableau d’octets est alloué du côté géré, il sera parfois collecté, vous n’avez pas à vous en occuper.

Pour quiconque lit encore,

Quelque chose que je ne pense pas avoir vu dans aucune des réponses – le code dangereux présente un risque de sécurité. Ce n’est pas un risque énorme, ce serait quelque chose de très difficile à exploiter. Toutefois, si, comme moi, vous travaillez dans une organisation conforme à la norme PCI, la stratégie interdit le code dangereux pour cette raison.

Le code managé est normalement très sécurisé car le CLR prend en charge l’emplacement et l’allocation de la mémoire, vous empêchant d’accéder ou d’écrire de la mémoire à laquelle vous n’êtes pas censé le faire.

Lorsque vous utilisez le mot-clé unsafe et que vous le comstackz avec / unsafe, utilisez des pointeurs, vous évitez ces vérifications et vous risquez d’utiliser quelqu’un pour accéder à un niveau non autorisé à la machine sur laquelle il s’exécute. En utilisant quelque chose comme une attaque par dépassement de tampon, votre code pourrait être amené à écrire des instructions dans une zone de mémoire à laquelle le compteur de programme (injection de code) pourrait accéder, ou simplement planter la machine.

Il y a de nombreuses années, SQL Server était en fait la cible de codes malveillants livrés dans un paquet TDS beaucoup plus long que prévu. La méthode de lecture du paquet ne vérifiait pas la longueur et continuait d’écrire le contenu au-delà de l’espace d’adressage réservé. La longueur et le contenu supplémentaires ont été soigneusement conçus de telle sorte qu’ils ont écrit un programme entier en mémoire, à l’adresse de la méthode suivante. L’attaquant faisait alors exécuter son propre code par le serveur SQL dans un contexte ayant le plus haut niveau d’access. Il n’a même pas eu besoin de casser le chiffrement car la vulnérabilité était inférieure à ce point dans la stack de la couche transport.