65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (43投票s)

2012年8月23日

CPOL

4分钟阅读

viewsIcon

62738

downloadIcon

864

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 上

请务必查看。

© . All rights reserved.