C#与C++互操作性能分析

在现代软件开发中,C#以其简洁的语法和强大的功能而广受欢迎。它通过消除手动内存管理的需求,提供了快速的编译时间、广泛的标准库以及其他实用功能。然而,对于需要大量数值计算的应用,其性能可能不尽如人意。本文将展示在必要时如何使用C#调用C++函数,并对其性能进行分析。

问题描述

假设有大量随机矩形分布在(0, 0)和(2, 2)之间,需要计算这些矩形中有多少百分比位于(0, 0)和(1, 1)之间。将使用暴力方法来解决这个问题,以测试CPU的性能。算法的代码相当直接:为每个矩形生成四个介于(0, 2)之间的随机数,并将它们分配给矩形的角点。然后,计算有多少矩形位于所需的区间内。所有测试都在配备4GB内存的q6600上进行,测试对象为1000万个矩形。

这是纯C#的实现,用作性能比较的基准。对于1000万个矩形,这种方法大约需要146毫秒。

互操作方案一:Marshaling

这是实现互操作的最简单方式。C#端的代码如下:

[DllImport("DllFuncs.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "nativef")] public static extern float getPercentBBMarshal( [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] BBox[] boxes, int size);

这告诉运行时在名为"DllFuncs.dll"的本地库中使用cdecl调用约定查找名为"nativef"的函数。它还告诉运行时将C#的BBox数组透明地转换为C++数组。需要传递大小,因为C++数组不知道它们的长度。对应的C++函数如下:

struct BBox { float x1, y1, x2, y2; int isValid() { return (x1 < 1) && (x2 < 1) && (y1 < 1) && (y2 < 1) && (x1 > 0) && (x2 > 0) && (y1 > 0) && (y2 > 0); } }; __declspec(dllexport) float __cdecl nativef(BBox * boxes, int size) { int sum = 0; for (int i = 0; i < size; i++) { sum += boxes[i].isValid(); } return (float)sum / (float)size * 100; }

Marshaling性能:使用计时器测量这个函数的性能,得到了一个有趣的结果。原生函数处理1000万个元素需要341毫秒,大约是C#等效函数所需时间的两倍!此外,对于1000个元素,Marshaling需要239.3毫秒,远远高于纯C#所需的0.054毫秒。显然,Marshaling增加了巨大的开销,随着工作量的增加,这种开销的相对重要性在减少。要了解这种开销的来源,需要知道Marshaling是如何工作的:

  • 为传递给函数的C#数组分配C++等效物。
  • C#数组的值复制到C++数组。
  • 调用C++函数。
  • 将C++函数的返回值复制到C#等效物。
  • 将控制权返回给C#汇编。

现在,可以很容易地看出性能不佳的原因。本质上是在分配1000万个矩形并复制它们的值。这是分配和移动大约160MB的数据!难怪性能如此糟糕。可能在问自己,为什么首先需要复制这个过程。有两个原因:

  • C#中结构体的内存布局可能与C++中的相同结构体的内存布局不匹配。因此,简单的指针赋值可能不起作用,因为C++可能与C#以不同的方式解释这个内存区域。
  • C#中的垃圾回收器可以自由地在内存中物理移动数据,以便进行压缩垃圾回收。因此,从C#传递到C++的指针可能在控制权到达C++时已经无效,因为GC可能已经将底层内存移动到了另一个物理位置。

那么,是否有可能解决这些问题呢?让找出答案!

互操作方案二:直接指针访问

解决内存布局问题相对简单。它只是告诉运行时像C++一样在内存中布局结构。C++使用某些对齐规则顺序布局数据,C#可以使用以下方式模仿:

[StructLayout(LayoutKind.Sequential)] struct BBox { public float x1, y1, x2, y2; // 矩形的角点 }

垃圾回收器问题可以通过使用fixed语句来解决。这个语句确保在语句的生命周期内内存不会被GC移动。fixed语句只能在编译时使用/unsafe选项的程序集中的不安全函数中使用。

[DllImport("DllFuncs.dll", CallingConvention = CallingConvention.Cdecl)] public static extern unsafe float nativef(IntPtr p, int size); public static unsafe float getPercentBBInterop(BBox[] boxes) { float result; fixed (BBox* p = boxes) { result = nativef((IntPtr)p, boxes.Length); } return result; }

指针访问性能:该函数返回时间为115毫秒,比C#等效函数快约26%。随着委托给原生代码的函数复杂性的增加,性能提升可能会增加。

性能数据

下面展示了处理不同数量元素时的性能图表:

https://github.com/debdattabasu/NativeInterop
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485