62

问题

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

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

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

示例代码

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

C 库 (DLL)

我的库

#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_

我的库

#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 应用程序

主程序

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

7 回答 7

45

这是一个有点旧的线程,但我最近在 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;
    }
}
于 2015-04-23T23:45:42.090 回答
14

互操作性中的注意事项解释了为什么以及何时需要编组以及成本是多少。引用:

  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);
    }
}
于 2018-01-10T13:59:09.687 回答
8

对于还在读书的人,

我认为我在任何答案中都没有看到 - 不安全的代码确实存在安全风险。这不是一个巨大的风险,这将是一个非常具有挑战性的利用。但是,如果您像我一样在符合 PCI 的组织中工作,由于这个原因,政策不允许不安全的代码。

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

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

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

于 2018-02-08T04:25:10.690 回答
5

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

如果您没有大型本机结构并且不介意所有数据都被填充两次 - 编组是更优雅且更安全的方法。

于 2015-02-12T16:38:50.570 回答
4

两个答案,

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

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

于 2013-07-15T06:18:32.157 回答
3

因为您声明您的代码调用了第 3 方 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[], ref int)对应的修改后的外部非托管函数CreateMyDataAlt(BYTE [], int)。一些数据复制和指针检查变得不必要了,也就是说,代码可以更简单并且可能运行得更快。

那么,修改后有什么不同呢?字节数组现在直接编组而不在 a 中扭曲struct并传递到非托管端。您不会在非托管代码中分配内存,而只是向其中填充数据(省略实现细节);并且在调用之后,需要的数据被提供给被管理方。如果你想呈现数据没有被填满,不应该被使用,你可以简单地设置length为零来告诉被管理方。因为字节数组是在托管端分配的,所以它会在某个时候被收集,你不必照顾它。

于 2018-01-05T20:11:06.580 回答
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%(等于总 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,回答 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

于 2018-10-27T06:50:38.363 回答