C# 本地互操作:方法和性能






4.72/5 (43投票s)
C#/C++互操作性介绍及性能评估。
引言
C# 是一门优秀的语言。它通过消除手动内存管理的需要,并提供快速的编译时间、广泛的标准库以及其他各种便捷的功能,使您能够极大地提高生产效率。但是,对于需要大量数值计算的应用程序,其性能可能不足。在本文中,我将向您展示 C# 在必要时如何调用 C++ 函数,并对其性能进行分析。
问题
假设在 (0, 0) 和 (2, 2) 之间存在大量随机矩形,让我们找到这些矩形中位于 (0, 0) 和 (1, 1) 之间的百分比。我们将使用暴力法来解决这个问题,以加大 CPU 的压力。此算法的代码非常简单。基本上,我们为每个矩形生成区间 (0, 2) 中的四个随机数,并将它们分配给角点。然后我们计算有多少个矩形位于所需的区间内。所有测试都在具有 4GB 内存的 q6600 上使用 1000 万个矩形进行。
C# 参考方案
这全部是 C# 代码,用于作为衡量相对性能的参考。对于 1000 万个矩形,此方法大约需要 146 毫秒。
互操作方法 1:编组
这是进行互操作最简单的方法。其 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# 的 BBoxe
数组透明地转换为 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;
}
编组性能
使用计时器测量此函数的性能,我们看到一个有趣的结果。对于 1000 万个元素,本机函数需要 341 毫秒,大约是 C# 等效函数所需时间的两倍!此外,对于 1000 个元素,编组需要 239.3 毫秒,远高于纯 C# 代码的 0.054 毫秒。当然,编组增加了巨大的开销,其相对重要性随着工作量的增加而减小。要了解这种开销来自哪里,我们需要了解编组的工作原理。
- 分配传递给函数的 C# 数组的 C++ 等效项。
- 将 C# 数组的值复制到 C++ 数组。
- 调用 C++ 函数。
- 将 C++ 函数的返回值复制到 C# 等效项。
- 将控制权返回给 C# 程序集。
现在,我们可以很容易地看出性能不佳的原因。我们实际上是在分配 1000 万个矩形并复制每个矩形的数值!也就是说,分配和移动大约 160 MB 的数据!难怪性能会很差。您可能想知道为什么首先需要进行整个复制操作。这有两个原因:
- C# 中结构的**内存布局**可能与 C++ 中相同结构的内存布局不匹配。因此,简单的指针赋值可能不起作用,因为 C++ 可能对该内存区域的解释与 C# 不同。
- C# 中的**垃圾回收器**可以自由地在内存中物理移动数据,以进行压缩垃圾回收。因此,从 C# 传递到 C++ 的指针在控制权到达 C++ 时可能无效,因为 GC 可能已将底层内存移动到另一个物理位置。
那么,是否有可能解决这些问题呢?让我们来看看!
互操作方法 2:直接指针访问
- **内存布局**问题很容易解决。这只是告诉运行时像 C++ 一样在内存中布局结构的问题。C++ 使用某些对齐规则顺序地布局数据,C# 可以使用以下方法模仿这些规则:
[StructLayout(LayoutKind.Sequential)]
struct BBox
{
public float x1, y1, x2, y2; //Corner points of the rectangle
}
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%。随着委托给原生代码的函数复杂性的增加,性能提升可能会增加。
性能数据
下图显示了不同处理元素数量下的性能。
示例代码
此代码的源代码托管在 Github 上
请务必查看。