web-dev-qa-db-fra.com

Performances C # - Utilisation de pointeurs dangereux au lieu d'IntPtr et de Marshal

Question

Je porte une application C en C #. L'application C appelle de nombreuses 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é les 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 avérée beaucoup plus lente que la version C. Une analyse rapide des performances a montré que la copie des 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. Puisque 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 du code et des pointeurs unsafe au lieu de IntPtr et Marshaling? Par exemple, est-ce plus dangereux (jeu de mots) en aucune façon? Les gens semblent préférer le marshaling, mais je ne sais pas pourquoi.
  2. L'utilisation de pointeurs pour P/Invocation est-elle vraiment plus rapide que l'utilisation du marshaling? Combien d'accélération peut-on espérer approximativement? Je n'ai trouvé aucun test de référence pour cela.

Exemple de code

Pour rendre la situation plus claire, j'ai piraté ensemble 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 et pointeurs dangereux" par rapport à "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 <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);
  }
}

Application C

Principal 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: %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);
  }
}
51
kol

C'est un peu vieux, mais j'ai récemment fait des tests de performances excessifs avec le marshaling en C #. J'ai besoin de démasquer beaucoup de données à partir d'un port série pendant plusieurs jours. Il était important pour moi de ne pas avoir de fuites de mémoire (car la plus petite fuite deviendra significative après quelques millions d'appels) et j'ai également fait beaucoup de tests de performances statistiques (temps utilisé) avec de très gros structs (> 10kb) juste pour le à cause de cela (un non, vous ne devriez jamais avoir une structure de 10 Ko :-))

J'ai testé les trois stratégies de dé-marshalling suivantes (j'ai aussi testé le marshalling). Dans presque tous les cas, le premier (MarshalMatters) a surpassé les deux autres. Maréchal: la copie a toujours été de loin la plus lente, les deux autres étant pour la plupart très proches dans la course.

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

Première:

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

Seconde:

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

Considérations en matière d'interopérabilité explique pourquoi et quand Marshaling est requis et à quel prix. Citation:

  1. Le marshaling se produit lorsqu'un appelant et un appelé ne peuvent pas opérer sur la même instance de données.
  2. un 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/Invoquer vraiment plus rapidement que d'utiliser le marshaling ...

posez-vous d'abord une question si le code managé peut fonctionner sur l'instance de valeur de retour de méthode non managée. Si la réponse est oui, Marshaling et le coût de performance associé ne sont pas requis. Le gain de temps approximatif serait O (n) fonction où n de la taille de l'instance marshallée. De plus, le fait de ne pas conserver à la fois des blocs de données gérés et non gérés en mémoire pendant la durée de la méthode (dans l'exemple "IntPtr and Marshal") élimine les frais généraux supplémentaires et la pression mémoire.

Quels sont les inconvénients de l'utilisation de code et de pointeurs dangereux ...

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

Il existe 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 managé. La meilleure pratique consiste à

CreateMyData(out myData1, length);

if(myData1!=IntPtr.Zero) {
    try {
        // -> use myData1
        ...
        // <-
    }
    finally {
        DestroyMyData(myData1);
    }
}
10
Serge Pavlov

Pour tous ceux qui lisent encore,

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

Le code managé est normalement très sécurisé car le CLR s'occupe de l'emplacement et de l'allocation de la mémoire, vous empêchant d'accéder ou d'écrire dans la mémoire que vous n'êtes pas censé utiliser.

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 obtenir un certain niveau d'accès non autorisé à la machine sur laquelle elle 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 qui pourrait ensuite être accessible par le compteur de programme (c'est-à-dire l'injection de code), ou simplement planter la machine.

Il y a de nombreuses années, SQL Server était en fait la proie d'un code malveillant livré dans un paquet TDS qui était beaucoup plus long qu'il ne devait l'ê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'adressage réservé. La longueur et le contenu supplémentaires ont été soigneusement conçus de telle sorte qu'il a écrit un programme entier en mémoire - à l'adresse de la méthode suivante. 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'a même pas eu besoin de casser le cryptage car la vulnérabilité était inférieure à ce point dans la pile de la couche de transport.

5
Simon Bridge

Deux réponses,

  1. Un code dangereux 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 les affectent. Mais l'utilisation de pointeurs sera certainement beaucoup plus rapide.

4
Palak.Maheria

Parce que vous avez déclaré que votre code appelle une DLL tierce, je pense que le code dangereux est plus adapté à votre scénario. Vous avez rencontré une situation particulière de échange de tableau de longueur variable dans un struct; Je sais, je sais que ce type d'utilisation se produit tout le temps, mais ce n'est pas toujours le cas après tout. Vous voudrez peut-être jeter un œil à certaines questions à ce sujet, par exemple:

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

Si .. je dis si .. vous pouvez modifier un peu les bibliothèques tierces 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 triage d'origine est commentée et déclarée CreateMyDataAlt(byte[], ref int) pour une fonction externe non gérée modifiée correspondante CreateMyDataAlt(BYTE [], int). Une partie de la copie des données et de la vérification du pointeur s'avère inutile, cela signifie que le code peut être encore plus simple et s'exécute probablement plus rapidement.

Alors, qu'est-ce qui est si différent avec la modification? Le tableau d'octets est désormais marshallé directement sans se déformer dans un struct et passé au côté non managé. Vous n'allouez pas la mémoire dans le code non managé, vous y remplissez simplement des 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 indiquer que les données ne sont pas remplies et ne doivent pas être utilisées, vous pouvez simplement définir length à zéro pour indiquer au côté géré. Parce que le tableau d'octets est alloué dans le côté géré, il sera collecté un jour, vous n'avez pas à vous en occuper.

3
Ken Kin

Je voulais juste ajouter mon expérience à ce vieux fil: Nous avons utilisé Marshaling dans un logiciel d'enregistrement sonore - nous avons reçu des données sonores en temps réel du mélangeur dans des tampons natifs et les avons rassemblées en octets []. C'était un vrai tueur de performances. Nous avons été obligés de passer à des structures dangereuses comme seul moyen de terminer la tâche.

Si vous n'avez pas de grandes structures natives 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.

3
Uldis Valneris

J'avais la même question aujourd'hui et je cherchais des valeurs de mesure concrètes, mais je n'en ai pas trouvé. J'ai donc écrit mes propres tests.

Le test consiste à copier les données de pixels d'une image RVB 10k x 10k. Les données d'image sont de 300 Mo (3 * 10 ^ 9 octets). Certaines méthodes copient ces données 10 fois, d'autres sont plus rapides et les copient donc 100 fois. Les méthodes de copie utilisées incluent

  • accès au tableau via un pointeur d'octets
  • Maréchal Copie (): a) 1 * 300 Mo, b) 1e9 * 3 octets
  • Buffer.BlockCopy (): a) 1 * 300 Mo, b) 1e9 * 3 octets

Environnement de test:
CPU: Intel Core i7-3630QM @ 2,40 GHz
Système d'exploitation: Win 7 Pro x64 SP1
Visual Studio 2015.3, le code est C++/CLI, la version .net ciblée est 4.5.2, compilée pour le débogage.

Résultats de test:
La charge du processeur est de 100% pour 1 cœur à toutes les méthodes (équivaut à 12,5% de la charge totale du processeur).
Comparaison de la vitesse et du temps d'exécution:

method                        speed   exec.time
Marshal.Copy (1*300MB)      100   %        100%
Buffer.BlockCopy (1*300MB)   98   %        102%
Pointer                       4.4 %       2280%
Buffer.BlockCopy (1e9*3B)     1.4 %       7120%
Marshal.Copy (1e9*3B)         0.95%      10600%

Délais d'exécution et débit moyen calculé écrits sous forme de commentaires dans le code ci-dessous.

//------------------------------------------------------------------------------
static void CopyIntoBitmap_Pointer (array<unsigned char>^ i_aui8ImageData,
                                    BitmapData^ i_ptrBitmap,
                                    int i_iBytesPerPixel)
{
  char* scan0 = (char*)(i_ptrBitmap->Scan0.ToPointer ());

  int ixCnt = 0;
  for (int ixRow = 0; ixRow < i_ptrBitmap->Height; ixRow++)
  {
    for (int ixCol = 0; ixCol < i_ptrBitmap->Width; ixCol++)
    {
      char* pPixel = scan0 + ixRow * i_ptrBitmap->Stride + ixCol * 3;
      pPixel[0] = i_aui8ImageData[ixCnt++];
      pPixel[1] = i_aui8ImageData[ixCnt++];
      pPixel[2] = i_aui8ImageData[ixCnt++];
    }
  }
}

//------------------------------------------------------------------------------
static void CopyIntoBitmap_MarshallLarge (array<unsigned char>^ i_aui8ImageData,
                                          BitmapData^ i_ptrBitmap)
{
  IntPtr ptrScan0 = i_ptrBitmap->Scan0;
  Marshal::Copy (i_aui8ImageData, 0, ptrScan0, i_aui8ImageData->Length);
}

//------------------------------------------------------------------------------
static void CopyIntoBitmap_MarshalSmall (array<unsigned char>^ i_aui8ImageData,
                                         BitmapData^ i_ptrBitmap,
                                         int i_iBytesPerPixel)
{
  int ixCnt = 0;
  for (int ixRow = 0; ixRow < i_ptrBitmap->Height; ixRow++)
  {
    for (int ixCol = 0; ixCol < i_ptrBitmap->Width; ixCol++)
    {
      IntPtr ptrScan0 = IntPtr::Add (i_ptrBitmap->Scan0, i_iBytesPerPixel);
      Marshal::Copy (i_aui8ImageData, ixCnt, ptrScan0, i_iBytesPerPixel);
      ixCnt += i_iBytesPerPixel;
    }
  }
}

//------------------------------------------------------------------------------
void main ()
{
  int iWidth = 10000;
  int iHeight = 10000;
  int iBytesPerPixel = 3;
  Bitmap^ oBitmap = gcnew Bitmap (iWidth, iHeight, PixelFormat::Format24bppRgb);
  BitmapData^ oBitmapData = oBitmap->LockBits (Rectangle (0, 0, iWidth, iHeight), ImageLockMode::WriteOnly, oBitmap->PixelFormat);
  array<unsigned char>^ aui8ImageData = gcnew array<unsigned char> (iWidth * iHeight * iBytesPerPixel);
  int ixCnt = 0;
  for (int ixRow = 0; ixRow < iHeight; ixRow++)
  {
    for (int ixCol = 0; ixCol < iWidth; ixCol++)
    {
      aui8ImageData[ixCnt++] = ixRow * 250 / iHeight;
      aui8ImageData[ixCnt++] = ixCol * 250 / iWidth;
      aui8ImageData[ixCnt++] = ixCol;
    }
  }

  //========== Pointer ==========
  // ~ 8.97 sec for 10k * 10k * 3 * 10 exec, ~ 334 MB/s
  int iExec = 10;
  DateTime dtStart = DateTime::Now;
  for (int ixExec = 0; ixExec < iExec; ixExec++)
  {
    CopyIntoBitmap_Pointer (aui8ImageData, oBitmapData, iBytesPerPixel);
  }
  TimeSpan tsDuration = DateTime::Now - dtStart;
  Console::WriteLine (tsDuration + "  " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));

  //========== Marshal.Copy, 1 large block ==========
  // 3.94 sec for 10k * 10k * 3 * 100 exec, ~ 7617 MB/s
  iExec = 100;
  dtStart = DateTime::Now;
  for (int ixExec = 0; ixExec < iExec; ixExec++)
  {
    CopyIntoBitmap_MarshallLarge (aui8ImageData, oBitmapData);
  }
  tsDuration = DateTime::Now - dtStart;
  Console::WriteLine (tsDuration + "  " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));

  //========== Marshal.Copy, many small 3-byte blocks ==========
  // 41.7 sec for 10k * 10k * 3 * 10 exec, ~ 72 MB/s
  iExec = 10;
  dtStart = DateTime::Now;
  for (int ixExec = 0; ixExec < iExec; ixExec++)
  {
    CopyIntoBitmap_MarshalSmall (aui8ImageData, oBitmapData, iBytesPerPixel);
  }
  tsDuration = DateTime::Now - dtStart;
  Console::WriteLine (tsDuration + "  " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));

  //========== Buffer.BlockCopy, 1 large block ==========
  // 4.02 sec for 10k * 10k * 3 * 100 exec, ~ 7467 MB/s
  iExec = 100;
  array<unsigned char>^ aui8Buffer = gcnew array<unsigned char> (aui8ImageData->Length);
  dtStart = DateTime::Now;
  for (int ixExec = 0; ixExec < iExec; ixExec++)
  {
    Buffer::BlockCopy (aui8ImageData, 0, aui8Buffer, 0, aui8ImageData->Length);
  }
  tsDuration = DateTime::Now - dtStart;
  Console::WriteLine (tsDuration + "  " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));

  //========== Buffer.BlockCopy, many small 3-byte blocks ==========
  // 28.0 sec for 10k * 10k * 3 * 10 exec, ~ 107 MB/s
  iExec = 10;
  dtStart = DateTime::Now;
  for (int ixExec = 0; ixExec < iExec; ixExec++)
  {
    int ixCnt = 0;
    for (int ixRow = 0; ixRow < iHeight; ixRow++)
    {
      for (int ixCol = 0; ixCol < iWidth; ixCol++)
      {
        Buffer::BlockCopy (aui8ImageData, ixCnt, aui8Buffer, ixCnt, iBytesPerPixel);
        ixCnt += iBytesPerPixel;
      }
    }
  }
  tsDuration = DateTime::Now - dtStart;
  Console::WriteLine (tsDuration + "  " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));

  oBitmap->UnlockBits (oBitmapData);

  oBitmap->Save ("d:\\temp\\bitmap.bmp", ImageFormat::Bmp);
}

informations connexes:
Pourquoi memcpy () et memmove () sont-ils plus rapides que les incréments de pointeur?
Array.Copy vs Buffer.BlockCopy , Réponse https://stackoverflow.com/a/33865267
https://github.com/dotnet/coreclr/issues/24 "Array.Copy & Buffer.BlockCopy x2 à x3 plus lent <1kB"
https://github.com/dotnet/coreclr/blob/master/src/vm/comutilnative.cpp , ligne 718 au moment de la rédaction: Buffer.BlockCopy() utilise memmove

2
Tobias Knauss