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

kol*_*kol 52 c c# pinvoke unsafe marshalling

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

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

  1. 使用unsafe代码和指针而不是IntPtrMarshaling有什么缺点?例如,它是否以任何方式更不安全(双关语)?人们似乎更喜欢编组,但我不知道为什么.
  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_
Run Code Online (Sandbox Code Playgroud)

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

Xan*_*vis 32

这是一个有点旧的线程,但我最近在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

第二:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

第三:

/// <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;
    }
}
Run Code Online (Sandbox Code Playgroud)


Ser*_*lov 10

互操作性中的注意事项解释了为什么以及何时需要编组以及以何种成本进行编组.引用:

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

因此,如果你回答你的问题

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

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

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

缺点是与通过指针直接访问存储器相关的风险.与在C或C++中使用指针相比,没有什么比这更安全了.如果需要使用它并且有意义.更多细节在这里.

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

CreateMyData(out myData1, length);

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


Uld*_*ris 5

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

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


小智 5

对于仍在阅读的人,

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

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

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

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