C#性能 - 使用不安全的指针而不是IntPtr和Marshal

时间:2013-07-09 13:14:12

标签: c# c pinvoke marshalling unsafe

问题

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

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

  1. 使用unsafe代码和指针代替IntPtrMarshal有什么缺点?例如,它是否以任何方式更不安全(双关语)?人们似乎更喜欢编组,但我不知道为什么。
  2. 使用P / Invoking指针真的比使用编组快吗?大约可以预期多少加速?我找不到任何基准测试。
  3. 示例代码

    为了使情况更加清晰,我将一个小的示例代码(真正的代码复杂得多)一起攻击。我希望这个例子说明我在谈论“不安全的代码和指针”与“IntPtr和Marshal”时的意思。

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

    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: %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();
    }
    

    C#应用程序,它使用IntPtrMarshal

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

    C#应用程序,它使用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);
      }
    }
    

7 个答案:

答案 0 :(得分:30)

这是一个有点旧的线程,但我最近在C#编组时进行了过多的性能测试。我需要在很多天内从串口解组大量数据。对我来说很重要的是没有内存泄漏(因为在几百万次调用之后,最小的泄漏会变得很大)并且我还使用非常大的结构(> 10kb)进行了大量的统计性能(使用时间)测试它的缘故(不,你永远不应该有10kb结构:-))

我测试了以下三种解组策略(我还测试了编组)。在几乎所有情况下,第一个(MarshalMatters)的表现优于其他两个。 Marshal.Copy到目前为止总是最慢的,其他两个人在比赛中大多非常接近。

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

首先:

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

第二

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

第三

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

答案 1 :(得分:9)

Considerations in Interoperability解释了为什么以及何时需要Marshaling以及付出多少代价。引用:

  
      
  1. 当调用者和被调用者无法对同一数据实例进行操作时,就会发生编组。
  2.   
  3. 重复编组可能会对您的应用程序的性能产生负面影响。
  4.   

因此,如果

回答你的问题
  

...使用P / Invoking的指针比使用编组更快......

首先问自己一个问题,托管代码是否能够在非托管方法返回值实例上运行。如果答案是肯定的,则不需要Marshaling和相关的性能成本。 节省的大致时间是 O(n) 函数,其中 n 的编组实例的大小。 此外,在方法持续时间内(在“IntPtr和Marshal”示例中)不同时将托管和非托管数据块同时保留在内存中,可以消除额外的开销和内存压力。

  

使用不安全的代码和指针有什么缺点......

缺点是与通过指针直接访问内存相关的风险。与在C或C ++中使用指针相比,没有什么比这更安全了。如果需要使用它并且有意义。更多详细信息为here

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

CreateMyData(out myData1, length);

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

答案 2 :(得分:4)

两个答案,

  1. 不安全代码表示它不受CLR管理。你需要照顾它使用的资源。

  2. 您无法扩展性能,因为影响它的因素太多了。但绝对使用指针会快得多。

答案 3 :(得分:4)

对于仍在阅读的人,

我不认为我在任何答案中看到的东西, - 不安全的代码确实存在安全风险。这不是一个巨大的风险,它将是一个非常具有挑战性的东西。但是,如果像我一样,您在兼容PCI的组织中工作,则政策不允许使用不安全的代码。

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

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

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

答案 4 :(得分:3)

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

如果您没有大型原生结构,并且不介意所有数据都填充两次 - Marshaling更优雅,更安全。

答案 5 :(得分:3)

因为您声明您的代码调用了第三方DLL,我认为 unsafe 代码更适合您的场景。您遇到了struct 中使用可变长度数组的特定情况;我知道,我知道这种用法一直在发生,但毕竟不是总是。您可能想看看有关此问题的一些问题,例如:

How do I marshal a struct that contains a variable-sized array to C#?

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

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设置为零即可告知管理方。因为字节数组是在托管端分配的,所以有时会收集它,你不需要处理它。

答案 6 :(得分:2)

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

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

  • 通过字节指针访问数组
  • Marshal.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%(等于12.5%的CPU总负载)。
速度和执行时间的比较:

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%

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

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

相关信息:
Why is memcpy() and memmove() faster than pointer increments?
Array.Copy vs 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