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

本文关键字:IntPtr Marshal 指针 性能 不安全 | 更新日期: 2023-09-27 18:21:35

问题

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

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

  1. 使用unsafe代码和指针代替IntPtrMarshal有什么缺点?例如,它在任何方面都更不安全(双关语)吗?人们似乎更喜欢编组,但我不知道为什么
  2. 对P/Invoking使用指针真的比使用封送处理快吗?预计大约能加速多少?我找不到这方面的任何基准测试

示例代码

为了让情况更清楚,我黑了一个小的示例代码(真正的代码要复杂得多)。我希望这个例子能说明我在谈论"不安全的代码和指针"与"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应用程序

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

程序.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代码和指针

程序.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#性能-使用不安全的指针而不是IntPtr和Marshal

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

我测试了以下三种解组策略(我还测试了编组)。在几乎所有情况下,第一个(MarshalMatters)的表现都优于其他两个。到目前为止,副本总是最慢的,其他两个在比赛中几乎靠得很近。

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

第一:

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. 当调用方和被调用方不能对同一数据实例进行操作时,就会发生封送
  2. 重复封送处理可能会对应用程序的性能产生负面影响

因此,如果

使用P/Invoking的指针比使用封送处理快得多。。。

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

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

缺点是通过指针直接访问内存的风险。没有什么比在C或C++中使用指针更安全的了。如果需要并且合理,请使用它。更多详细信息请点击此处。

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

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

对于任何仍在阅读的人,

我想我在任何答案中都没有看到,不安全的代码确实存在某种安全风险。这不是一个巨大的风险,利用它将是一个相当具有挑战性的东西。但是,如果您像我一样在PCI兼容的组织中工作,则出于这个原因,策略不允许使用不安全的代码。

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

当你使用unsafe关键字并使用"/ansafe"和指针进行编译时,你绕过了这些检查,并为某人使用你的应用程序获得对其运行的机器的某种程度的未经授权的访问创造了可能性,您的代码可能会被诱骗将指令写入内存区域,然后程序计数器可能会访问该区域(即代码注入),或者只是使机器崩溃。

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

我只是想把我的经验添加到这个旧线程中:我们在录音软件中使用了编组——我们将混音器中的实时声音数据接收到本地缓冲区中,并将其编组为byte[]。这是真正的表演杀手。我们被迫转移到不安全的结构,这是完成任务的唯一途径。

如果您没有大型的本地结构,并且不介意所有数据都被填充两次,那么封送是一种更优雅、更安全的方法。

两个答案,

  1. 不安全代码意味着它不受CLR管理。你需要注意它使用的资源。

  2. 你无法缩放性能,因为有太多因素影响它。但使用指针肯定会更快。

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

如何将包含可变大小数组的结构封送至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 [], int)声明了CreateMyDataAlt(byte[], ref int)。一些数据复制和指针检查变得不必要,也就是说,代码可以更简单,运行速度可能更快。

那么,修改有什么不同呢?字节数组现在在struct中被直接编组而不进行扭曲,并被传递到非托管端。您不在非托管代码中分配内存,而只是向其中填充数据(省略了实现细节);并且在呼叫之后将所需的数据提供给被管理侧。如果您想表示数据未填充且不应使用,您可以简单地将length设置为零来告知被管理方。因为字节数组是在托管端中分配的,所以它会在某个时候被收集,您不必考虑这一点。

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

测试是复制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,编译用于调试。

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

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

相关信息:
为什么memcpy()和memmove()比指针增量快
Array.Copy与Buffer.BlockCopy,Answerhttps://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