具有许多C++互操作调用的自动C#垃圾回收器性能较差



是什么原因导致C#垃圾回收在我的C#应用程序中失败得如此之惨?当一个位置合适的GC.Collect解决了这个问题时,它会执行大量C++调用?我的C#应用程序使用System.Runtime.InteropServices DllImport和CallingConvention.Cdecl进行了数百万次C++调用,并有一些C#析构函数来释放一些C++非托管内存。我使用的是.NET Framework 4。

  1. 是什么原因导致我的应用程序中的以下代码强制分页,将执行速度减慢到爬网(在我的32 GB系统上消耗29 GB的RAM,在我终止进程之前需要4分钟以上),而只是将ManualGC更改为true将内存使用量限制在600MB左右,执行在29秒内完成?

  2. 为什么保留ManualGC false并将Write更改为true会将内存使用量限制在大约12GB,并允许在大约59秒内完成执行而不进行分页?

我的应用程序中的一些代码片段(显然更改了一些名称):

private static int callCount = 0;
private const bool ManualGC = false;
private const bool Write    = false;
internal static void CommonlyCalled()
{
    ++callCount;
    if ( callCount % 100000 == 0)
    {
        if (ManualGC)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        if (Write) Console.WriteLine(HandleErrorsCallCount);
    }
    DoLogic();
}

内存是用Windows任务管理器中的"内存(专用工作集)"列来测量的。行为始终是可重复的。

并且有一些C#析构函数来释放一些C++非托管内存

如果包装器很小,并且C++代码需要大量内存,那么这还不够。你只是没有给GC施加足够的压力,让它尽快调用你的终结器。实现IDisposable来解决这个问题是样板。但并不是一个完整的解决方案,你应该告诉GC这一点,这样它就可以对此做点什么

using System;
using System.Runtime.InteropServices;
class Program {
    static void Main(string[] args) {
        while (!Console.KeyAvailable) {
            new Wrapper();
        }
    }
}
class Wrapper {
    private const int alloc = 10 * 1024;    // C++ object memory usage
    private readonly bool useamp = false;   // Change this after testing
    private IntPtr mem;
    public Wrapper() {
        if (useamp) GC.AddMemoryPressure(alloc);
        mem = Marshal.AllocHGlobal(alloc);
    }
    ~Wrapper() {
        Marshal.FreeHGlobal(mem);
        if (useamp) GC.RemoveMemoryPressure(alloc);
    }
}

当你运行这个程序的时候,观察一下它的内存使用情况。在我的机器上,私有字节大约有半个字节。现在更改useamp并再次运行,您将看到它的效率大大提高,只需要4MB。不必调用Dispose:)在Win 8.1、.NET 4.5.1上测试,在较旧的.NET版本上可能会得到非常不同的结果。

您为alloc选择的值并没有那么关键,它只需要在大致范围内。显然,您确实需要超过10KB的容量。

这听起来像是在依赖一个用户定义的终结器来实现C#应用程序中的正确行为。您需要更新您的C#代码来解决此场景中的以下特定问题:

  1. 您不应该使用用户定义的终结器1(语法类似C++析构函数的C#方法)。如果您有一个需要清理的非托管资源,请创建一个扩展SafeHandle的类,只封装该资源的句柄
  2. 编写代码时应始终在SafeHandle实例上显式调用Dispose(),这些实例在使用完非托管资源后封装非托管资源。永远不要依赖C#中的终结器来执行清理操作

1这句话在以下几乎是一条生活规则的情况下仍然成立:

如果您用C#编写了一个用户定义的终结器,那么您可能做错了什么。

最新更新