关于C#性能:C#性能-使用不安全的指针代替IntPtr和Marshal

C# performance - Using unsafe pointers instead of IntPtr and Marshal

我正在将C应用程序移植到C#中。 C应用程序从第三方DLL调用了许多函数,因此我在C#中为这些函数编写了P / Invoke包装器。其中一些C函数分配了我必须在C#应用程序中使用的数据,因此我使用了IntPtrMarshal.PtrToStructureMarshal.Copy来将本机数据(数组和结构)复制到托管变量中。

不幸的是,事实证明C#应用程序比C版本慢得多。快速性能分析表明,上述基于封送处理的数据复制是瓶颈。我正在考虑通过重写C#代码以使用指针来加速它。由于我没有使用C#中不安全的代码和指针的经验,因此我需要有关以下问题的专家意见:

  • 使用unsafe代码和指针而不是IntPtrMarshal ing有什么缺点?例如,它在某种程度上更不安全(双关语)吗?人们似乎更喜欢封送,但我不知道为什么。
  • 使用指针进行P /调用真的比使用封送处理快吗?大约可以预期提高多少速度?我找不到任何基准测试。
  • 范例程式码

    为了使情况更清楚,我整理了一个小的示例代码(实际代码要复杂得多)。我希望这个例子能说明我在谈论"不安全的代码和指针"与" IntPtr and Marshal"时的意思。

    C库(DLL)

    MyLib.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #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应用

    Main.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <stdio.h>
    #include"MyLib.h"

    void main()
    {
      struct MyData* myData = NULL;
      int length = 100 * 1024 * 1024;

      printf("=== C++ test ===\
    "
    );
      CreateMyData(&myData, length);
      if (myData != NULL)
      {
        printf("Length: %d\
    "
    , myData->length);
        if (myData->bytes != NULL)
          printf("First: %d, last: %d\
    "
    , myData->bytes[0], myData->bytes[myData->length - 1]);
        else
          printf("myData->bytes is NULL");
      }
      else
        printf("myData is NULL\
    "
    );
      DestroyMyData(myData);
      getchar();
    }

    C#应用程序,使用IntPtrMarshal

    Program.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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#应用程序,使用unsafe代码和指针

    Program.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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#中使用封送处理进行了过多的性能测试。我需要在许多天内从串行端口解组大量数据。对于我来说,重要的是没有内存泄漏(因为最小的泄漏将在数百万次调用后变得很明显),并且我还针对大型结构(> 10kb)进行了大量的统计性能(使用时间)测试因此(不,你永远不应该有一个10kb的结构:-))

    我测试了以下三种编组策略(也测试了编组)。在几乎所有情况下,第一个(MarshalMatters)都胜过其他两个。
    元帅到目前为止,复制总是最慢的,其他两个在比赛中几乎是非常接近的。

    使用不安全的代码可能会带来重大的安全风险。

    第一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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;
        }
    }

    第二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    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;
        }
    }

    第三:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    /// <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;
        }
    }


    互操作性中的注意事项说明了为什么以及何时需要封送处理以及需要多少费用。引用:

  • Marshaling occurs when a caller and a callee cannot operate on the same instance of data.
  • repeated marshaling can negatively affect the performance of your application.
  • 因此,如果回答您的问题,

    ... using pointers for P/Invoking really faster than using marshaling ...

    首先要问自己一个问题,托管代码是否能够在非托管方法的返回值实例上进行操作。如果答案是肯定的,则不需要封送处理和相关的性能成本。
    节省的时间大约是O(n)函数,其中n是编组实例的大小。
    另外,在该方法的持续时间内(在" IntPtr and Marshal"示例中),不能同时将托管数据块和非托管数据块同时保留在内存中,这消除了额外的开销和内存压力。

    What are the drawbacks of using unsafe code and pointers ...

    缺点是与直接通过指针访问内存相关的风险。没有比使用C或C ++中的指针更安全的了。如有必要,请使用它并有意义。更多细节在这里。

    所提供的示例存在一个"安全"问题:在托管代码错误之后,不能保证释放分配的非托管内存。最佳做法是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    CreateMyData(out myData1, length);

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


    对于仍在阅读的人,

    我认为我没有在任何答案中看到某些东西,-不安全的代码确实存在安全隐患。这不是一个巨大的风险,要利用它将具有很大的挑战性。但是,如果像我一样在PCI兼容组织中工作,则出于这个原因,政策会禁止使用不安全的代码。

    托管代码通常非常安全,因为CLR负责内存的位置和分配,从而防止您访问或写入您不应该使用的任何内存。

    当您使用unsafe关键字并使用'/ unsafe'进行编译并使用指针时,您将绕过这些检查,并可能使某人使用您的应用程序来获得对其运行计算机的某种程度的未授权访问。使用诸如缓冲区溢出攻击之类的方法,您的代码可能被欺骗,将指令写到内存区域中,然后程序计数器可以访问指令(即代码注入),或者只是使机器崩溃。

    许多年前,SQL Server实际上是TDS数据包中传递的恶意代码的牺牲品,而TDS数据包中的恶意代码比原先预期的要长得多。读取数据包的方法没有检查长度,而是继续将内容写入保留的地址空间之后。精心设计了额外的长度和内容,以便将整个程序写入内存-在下一个方法的地址处。
    然后,攻击者在访问级别最高的上下文中,由SQL Server执行自己的代码。它甚至不需要中断加密,因为该漏洞在传输层堆栈中低于此点。


    两个答案,

  • 不安全的代码意味着它不是由CLR管理的。您需要照顾它使用的资源。

  • 您无法缩放性能,因为影响它的因素太多。但是绝对可以使用指针更快。


  • 因为您说过您的代码调用了第三方DLL,所以我认为不安全的代码更适合您的情况。您遇到了在struct中包装可变长度数组的特殊情况;我知道,我知道这种用法一直在发生,但毕竟并非总是如此。您可能想看看有关此的一些问题,例如:

    如何将包含可变大小数组的结构编组到C#?

    如果..我说如果..您可以针对这种特殊情况稍微修改第三方库,那么您可以考虑以下用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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);
        }
    }

    如您所见,很多原始的编组代码都被注释掉了,并声明了CreateMyDataAlt(byte[], ref int)来表示与内核相关的修改后的外部非托管函数CreateMyDataAlt(BYTE [], int)。不需要进行某些数据复制和指针检查,也就是说,代码可以更简单,并且运行速度可能更快。

    那么,修改有何不同?现在,直接将字节数组编组,而不会在struct中扭曲,并将其传递到非托管端。您无需在非托管代码中分配内存,而只是向其中填充数据(实现细节已省略);通话后,所需的数据将提供给被管理方。如果要显示未填充数据且不应使用该数据,则可以简单地将length设置为零以告知被管理方。因为字节数组是在受管端分配的,所以它会在某个时间被收集,您不必担心。


    只是想将我的经验添加到这个旧线程中:
    我们在录音软件中使用了封送处理(Marshaling)-我们从混音器接收实时声音数据到本机缓冲区中,并将其封送为byte []。那才是真正的性能杀手。我们被迫使用不安全的结构作为完成任务的唯一方法。

    如果您没有大型本机结构,并且不介意所有数据都被填充两次-封送处理更优雅,更安全。


    今天我有同样的问题,我一直在寻找一些具体的测量值,但找不到。所以我写了自己的测试。

    测试是复制10k x 10k RGB图像的像素数据。图像数据为300 MB(3 * 10 ^ 9字节)。一些方法可以将此数据复制10次,而其他方法则更快,因此可以将其复制100次。所使用的复制方法包括

    • 通过字节指针访问数组
    • 元帅.Copy():a)1 * 300 MB,b)1e9 * 3字节
    • Buffer.BlockCopy():a)1 * 300 MB,b)1e9 * 3字节

    测试环境:
    CPU:Intel Core i7-3630QM @ 2.40 GHz
    操作系统:Win 7 Pro x64 SP1
    Visual Studio 2015.3,代码为C ++ / CLI,目标.net版本为4.5.2,已针对Debug进行编译。

    试验结果:
    在所有方法中,1核的CPU负载均为100%(等于总CPU负载的12.5%)。
    速度和执行时间的比较:

    1
    2
    3
    4
    5
    6
    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%

    执行时间和计算出的平均吞吐量在下面的代码中以注释形式编写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    //------------------------------------------------------------------------------
    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);
    }

    相关信息:
    为什么memcpy()和memmove()比指针增量快?
    Array.Copy与Buffer.BlockCopy,回答https://stackoverflow.com/a/33865267
    https://github.com/dotnet/coreclr/issues/2430" Array.Copy&Buffer.BlockCopy x2到x3慢于<1kB" https://github.com/dotnet/coreclr/blob/master/src/vm/comutilnative.cpp,撰写本文时为第718行:Buffer.BlockCopy()使用memmove