C # performance-utiliser des indicateurs dangereux au lieu de L'IntPtr et du Marshal

Question

je transfère une application C dans C#. L'application c appelle beaucoup de fonctions d'une DLL tierce partie, donc j'ai écrit P/Invoke wrappers pour ces fonctions dans C#. Certaines de ces fonctions C allouer des données que je dois utiliser dans le c # app, donc j'ai utilisé IntPtr de l ', Marshal.PtrToStructure et Marshal.Copy pour copier les données natives (tableaux et structures) en variables gérées.

Malheureusement, le c # app s'est avéré beaucoup plus lent que la version C. Une analyse rapide des performances a montré que la copie de données basée sur le marshaling mentionnée 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 et les indicateurs dangereux dans C#, j'ai besoin de l'opinion d'un expert concernant les questions :

  1. Qu'est-ce que le inconvénients d'utiliser le code et les pointeurs unsafe au lieu de IntPtr et Marshal ing? Par exemple, est-il plus dangereux (jeu de mots prévu) d'une façon ou d'une autre? Les gens semblent préférer la police, mais je ne sais pas pourquoi.
  2. est-ce que L'utilisation de pointeurs pour P/Invoking est vraiment plus rapide que l'utilisation de marshaling? Combien d'accélération peut-on attendre approximativement? Je n'ai pas trouvé de test de référence pour ça.

exemple de code

à faire la situation est plus claire, j'ai hacké un petit exemple de code (le vrai code est beaucoup plus complexe). J'espère que cet exemple montre ce que je veux dire quand je parle de "code dangereux et des indicateurs" vs. "IntPtr et Marshal".

C library (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 <stdlib.h>
#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 < length; ++i)
        (*myData)->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

Main.c

#include <stdio.h>
#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: %dn", myData->length);
    if (myData->bytes != NULL)
      printf("First: %d, last: %dn", myData->bytes[0], myData->bytes[myData->length - 1]);
    else
      printf("myData->bytes is NULL");
  }
  else
    printf("myData is NULLn");
  DestroyMyData(myData);
  getchar();
}

C# application, qui utilise IntPtr et Marshal

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

C # application, qui utilise unsafe code et pointeurs

programme.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);
  }
}
49
demandé sur kol 2013-07-09 17:14:11

6 réponses

c'est un peu un vieux fil, mais j'ai récemment fait des tests de performance excessive avec marshaling en C#. J'ai besoin de démonter un grand nombre de données d'un port série pendant plusieurs jours. Il était important pour moi de ne pas avoir de fuites de mémoire (parce que la plus petite fuite va devenir significative après quelques millions d'appels) et j'ai aussi fait beaucoup de tests de performance statistique (temps utilisé) avec de très grandes structures (>10kb) juste pour le plaisir (un non, vous ne devriez jamais avoir une structure de 10kb :-) )

j'ai testé les trois stratégies suivantes de désembuage (j'ai aussi testé le marshalling). Dans presque tous les cas, le premier (MarshalMatters) a obtenu de meilleurs résultats que les deux autres. Maréchal.La copie était toujours la plus lente de loin, les deux autres étaient pour la plupart très proches dans la course.

L'utilisation d'un code non sécuritaire peut présenter un risque important pour la sécurité.

d'Abord:

public class MarshalMatters
{
    public static T ReadUsingMarshalUnsafe<T>(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>(selectedT structure) where selectedT : struct
    {
        byte[] byteArray = new byte[Marshal.SizeOf(structure)];
        fixed (byte* byteArrayPtr = byteArray)
        {
            Marshal.StructureToPtr(structure, (IntPtr)byteArrayPtr, true);
        }
        return byteArray;
    }
}

la Deuxième:

public class Adam_Robinson
{

    private static T BytesToStruct<T>(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;
    }

    /// <summary>
    /// no Copy. no unsafe. Gets a GCHandle to the memory via Alloc
    /// </summary>
    /// <typeparam name="selectedT"></typeparam>
    /// <param name="structure"></param>
    /// <returns></returns>
    public static byte[] StructToBytes<T>(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:

/// <summary>
/// http://stackoverflow.com/questions/2623761/marshal-ptrtostructure-and-back-again-and-generic-solution-for-endianness-swap
/// </summary>
public class DanB
{
    /// <summary>
    /// uses Marshal.Copy! Not run in unsafe. Uses AllocHGlobal to get new memory and copies.
    /// </summary>
    public static byte[] GetBytes<T>(T structure) where T : struct
    {
        var size = Marshal.SizeOf(structure); //or Marshal.SizeOf<selectedT>(); 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<T>(byte[] bytes) where T : struct
    {
        var structure = new T();
        int size = Marshal.SizeOf(structure);  //or Marshal.SizeOf<selectedT>(); 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;
    }
}
26
répondu Xan-Kun Clark-Davis 2018-03-03 19:14:19

Considérations relatives à l'Interopérabilité explique pourquoi et quand Regroupement est nécessaire et à quel coût. Citation:

  1. il y a appel Lorsqu'un appelant et un destinataire ne peuvent pas utiliser la même instance de données.
  2. la mise en place répétée peut affecter négativement la performance de votre application.

donc, répondre à votre question si

... utiliser des pointeurs pour P / invoquer vraiment plus vite que d'utiliser la marshaling ...

posez-vous d'abord la question de savoir 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, alors Marshaling et le coût n'est pas nécessaire. L'économie de temps approximative serait O(n) fonction où n de la taille de l'mobilisé l'instance. De plus, le fait de ne pas garder en mémoire les blocs de données gérés et non gérés en même temps pendant toute la durée de la méthode (dans L'exemple "IntPtr et Marshal") élimine les frais généraux supplémentaires et la pression de mémoire.

Quels sont les inconvénients d'utiliser un code et des pointeurs dangereux ...

L'inconvénient est le risque associé à l'accès la mémoire directement à travers des pointeurs. Il n'y a rien de moins sûr que d'utiliser des pointeurs en C ou C++. Utiliser si nécessaire et de la logique. Plus de détails sont ici .

il y a une" préoccupation 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ées. La meilleure pratique est de

CreateMyData(out myData1, length);

if(myData1!=IntPtr.Zero) {
    try {
        // -> use myData1
        ...
        // <-
    }
    finally {
        DestroyMyData(myData1);
    }
}
6
répondu Serge Pavlov 2018-01-11 16:39:02

deux réponses,

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

  2. vous ne pouvez pas évaluer la performance parce qu'il y a tant de facteurs qui l'affectent. Mais certainement utiliser des pointeurs sera beaucoup plus rapide.

4
répondu Palak.Maheria 2014-06-21 13:57:24

je voulais Juste ajouter mon expérience à ce vieux thread: Nous avons utilisé le Marshaling dans un logiciel d'enregistrement sonore - nous avons reçu des données sonores en temps réel de mixer dans des tampons indigènes et nous les avons regroupées en octet[]. C'était un vrai tueur de performance. Nous avons été forcés de passer à des structures dangereuses comme seul moyen d'accomplir la tâche.

dans le cas où vous n'avez pas de grandes structures natives et ne vous souciez pas que toutes les données sont remplies deux fois - Marshaling est plus élégant et beaucoup, approche beaucoup plus sûr.

3
répondu Uldis Valneris 2015-07-07 20:36:35

parce que vous avez déclaré que votre code appelle à un tiers DLL, je pense que le dangereux code est plus adapté dans vous scénario. Vous avez rencontré une situation particulière de Wapping tableau de longueur variable dans un struct ; je sais, je sais que ce genre d'usage se produit tout le temps, mais ce n'est pas toujours le cas après tout. Vous pourriez vouloir jeter un oeil à quelques questions à ce sujet, par exemple:

Comment puis-je marsher une structure qui contient un tableau de taille variable à C#?

si .. Je dis que si .. vous pouvez modifier un peu les bibliothèques de tiers pour ce cas particulier, alors vous pourriez considérer l'usage suivant:

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 composition original est commentée et déclaré CreateMyDataAlt(byte[], ref int) pour une fonction externe non gérée CreateMyDataAlt(BYTE [], int) . Une partie de la copie de données et pointeur de vérifier s'avère être inutile, qui dit, le code peut être encore plus simple et probablement plus rapide.

alors, qu'est-ce qu'il y a de si différent avec la modification? Le byte array est maintenant placé directement sans protection dans un struct et passé du côté non-géré. Vous n'attribuez pas la mémoire dans le code non géré, mais vous remplissez simplement les données(les détails de l'implémentation ont été omis); et après l'appel, les données nécessaires sont fournies au côté géré. Si vous vous voulez présenter que les données ne sont pas remplies et ne devraient pas être utilisées, vous pouvez simplement mettre length à zéro pour dire le côté géré. Parce que le tableau byte est alloué dans le côté géré, il sera collecté un jour, vous n'avez pas à vous en occuper.

3
répondu Ken Kin 2018-01-11 03:23:25

Pour ceux qui sont encore en lecture,

quelque chose que je ne pense pas avoir vu dans les réponses, - code dangereux présente quelque chose d'un risque de sécurité. Ce n'est pas un risque énorme, ce serait quelque chose de très difficile à exploiter. Cependant, si comme moi vous travaillez dans une organisation conforme PCI, le code dangereux est refusé par la politique pour cette raison.

Le code géré

est normalement très sûr parce que le CLR s'occupe de l'emplacement et de l'allocation de la mémoire, vous empêcher d'accéder ou d'écrire un souvenir que vous n'êtes pas censé écrire.

lorsque vous utilisez le mot-clé unsafe et compilez avec '/unsafe' et utilisez des pointeurs, vous contournez ces vérifications et créez la possibilité pour quelqu'un d'utiliser votre application pour gagner un certain niveau d'accès non autorisé à la machine sur laquelle elle tourne. En utilisant quelque chose comme une attaque de dépassement de tampon, votre code pourrait être piégé dans l'écriture d'instructions dans une zone de mémoire qui pourrait alors être accédé par le programme counter (i.e. code injection), ou tout simplement planter la machine.

il y a de nombreuses années, SQL server était en fait la proie du code malveillant livré dans un paquet TDS qui était beaucoup plus long que ce qu'il était censé être. La méthode de lecture du paquet n'a pas vérifié la longueur et a continué à écrire le contenu au-delà de l'espace d'adresse réservé. La longueur et le contenu supplémentaires ont été soigneusement conçus de sorte qu'il a écrit un programme entier dans la mémoire - à l'adresse de la méthode suivante. Le l'attaquant avait alors son propre code exécuté par le serveur SQL dans un contexte qui avait le plus haut niveau d'accès. Il n'avait même pas besoin de casser le cryptage car la vulnérabilité était en dessous de ce point dans la pile de la couche de transport.

2
répondu Simon Bridge 2018-02-08 04:25:10