创建不带 GuidAttribute 的 .NET COM 对象实例时出现内存泄漏和巨大的性能下降

发布于 2025-01-06 12:47:55 字数 2733 浏览 0 评论 0 原文

考虑以下我们可以在 C# 中定义的最简单 COM 对象的示例(使用带有 .NET Framework 4.0 的 Visual Studio 2010 SP1 构建):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

namespace CcwTestLib
{
   [ComVisible(true)]   
   [Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]  
   public class CcwTestObject
   {

   }
}

我们编译此程序集并使用 regasm(或 Visual Studio 中的内置选项)将其注册为 COM 互操作。

现在,我们只需用 C++ 编写一个非托管 Win32 控制台应用程序,该应用程序除了创建该对象的实例并释放它 100,000 次之外,什么也不做。例如,使用以下程序:

#include "stdafx.h"

// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject = 
   { 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };

int _tmain(int argc, _TCHAR* argv[])
{
   CoInitializeEx(NULL,  COINIT_MULTITHREADED);

   IUnknown *pTestObject = NULL;

   const int iCount = 100000;

   wprintf(L"Allocating COM instance %i times...\n", iCount);

   for (int i = 0; i < iCount; i++)
   {
      HRESULT hr = CoCreateInstance(CLSID_CcwTestObject, 
                                    NULL, 
                                    CLSCTX_INPROC_SERVER, 
                                    IID_IUnknown, 
                                    (LPVOID*)&pTestObject);
      if (FAILED(hr))
      {
         wprintf(L"Error: %i", hr);
         return -1;
      }

      pTestObject->Release();
   }

   CoUninitialize();

   return 0;
}

在本地系统上运行此应用程序时,它在大约 820 毫秒内完成,消耗大约 32MB 内存。将 iCount 增加到 10,000,000 会使程序需要更长的时间才能完成(当然),但查看内存消耗,它会增加到大约 92MB,并在程序执行的剩余时间内保持该状态。到目前为止没有什么太奇怪的。

现在是有趣的部分,引出了我的问题。让我们从 .NET 类中删除 Guid 属性(并禁用自动 COM 注册(如果已启用),以便以前的注册在注册表中仍然完好无损)并重建程序集。

我们再次运行测试程序,并将 iCount 设置为 100,000。这次程序大约在90,000ms内完成!这比以前慢了大约100 倍

更有趣和麻烦的是当我们将 iCount 增加到 10,000,000 并启动程序时。如果我们使用 Process Explorer 或 VMMap 或类似程序监控其内存消耗,我们可以看到它慢慢增加,但它不会像我们预期的那样停留在 92MB。相反,它似乎会永远持续下去。据推测,当用完大约 2GB 的虚拟内存空间(因为它是 x86 进程)时,应用程序将会崩溃,但由于它的运行速度如此之慢,我们没有等到这种情况发生,而是在 1,200MB 左右退出。

应该注意的是,使用 COM 对象、调用其方法等等(如果我们定义了任何方法)都可以正常工作,因为创建对象的所有必要信息都存储在注册表中。我们系统上的一部分如下所示:

[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll

其中 CLSID 正确指向程序集及其代码库,以及程序集中的显式类型。

我们还发现,将属性中的 Guid 更改为注册的 Guid 以外的任何值都会产生同样的问题。

那么为什么会发生这种情况呢?这是 .NET 中的错误吗?这个问题有什么解决办法吗?

我很高兴能够深入了解这个问题,我们花了大约一周的时间才从我们产品中发现的内存泄漏中缩小到这个更加简化的场景。

Consider the following example of the simplest COM object we can define in C# (built using Visual Studio 2010 SP1 with .NET framework 4.0):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

namespace CcwTestLib
{
   [ComVisible(true)]   
   [Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]  
   public class CcwTestObject
   {

   }
}

We compile this assembly and register it for COM interop using regasm (or the built in option in Visual Studio).

Now we simply write an unmanaged Win32 console application in C++ that does nothing other than to create an instance of this object and release it 100,000 times. For example using the following program:

#include "stdafx.h"

// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject = 
   { 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };

int _tmain(int argc, _TCHAR* argv[])
{
   CoInitializeEx(NULL,  COINIT_MULTITHREADED);

   IUnknown *pTestObject = NULL;

   const int iCount = 100000;

   wprintf(L"Allocating COM instance %i times...\n", iCount);

   for (int i = 0; i < iCount; i++)
   {
      HRESULT hr = CoCreateInstance(CLSID_CcwTestObject, 
                                    NULL, 
                                    CLSCTX_INPROC_SERVER, 
                                    IID_IUnknown, 
                                    (LPVOID*)&pTestObject);
      if (FAILED(hr))
      {
         wprintf(L"Error: %i", hr);
         return -1;
      }

      pTestObject->Release();
   }

   CoUninitialize();

   return 0;
}

When running this application on our local system it completed in about 820ms and consumes about 32MB memory. Increasing iCount to 10,000,000 makes the program take a lot longer to complete (of course) but looking at the memory consumption it increases to about 92MB and stays there for the remainder of the execution of the program. Nothing too strange so far.

Now for the interesting part, leading up to my question. Let's remove the Guid attribute from the .NET class (and disable automatic COM registration if enabled so that the previous registration is still left intact in the registry) and rebuild the assembly.

We run the test program again with iCount set to 100,000. This time the program completes in about 90,000ms! That is about 100 times slower than before!

Even more interesting and troublesome is when we increase iCount to 10,000,000 and start the program. If we monitor its memory consumption using Process Explorer or VMMap or a similar program we can see it slowly increase, but it doesn’t stop at 92MB as we might expect. Instead it seems to continue on forever. Presumably the application will crash when running out of virtual memory space at around 2GB (since it is an x86 process), but since it’s moving so slow we didn’t wait for that to happen in this test but quit around 1,200MB.

It should be noted that using the COM object, calling its methods and so on (if we had defined any) works just fine, as it should since all the necessary information for creating the object is stored in the registry. A part of it on our system looks like the following:

[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll

Where the CLSID correctly points to the assembly and its codebase, and the explicit type within the assembly.

We also discovered that altering the Guid in the attribute to anything other than the one it is registered with creates the very same problem.

So why is this happening? Is this a bug in .NET? And is there any workaround to this problem?

I would be very happy for some insight into this problem, which took us about a week to narrow down to this much simplified scenario from a discovered memory leak in our product.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

总以为 2025-01-13 12:47:55

看来内存泄漏是一个错误,已在 .NET 4.5 中修复。

有关更多详细信息,请参阅问题 Microsoft Connect

引用他们的回答:

内存泄漏已修复(.NET Framework 4.5 开发人员预览版是第一个修复此问题的公开版本)。至于速度慢,Guid 不匹配有效地停用了我们的内部缓存,因此我们在每次激活时执行一系列注册表查找和按名称类型查找。由于这不是主线场景,并且应仅限于在未更新注册的情况下重新引导课程的情况,因此我们暂时不优先考虑此问题。未来版本仍对其进行跟踪。

It seems the memory leak was a bug that has been fixed in .NET 4.5.

For more details see the issue at Microsoft Connect.

To quote from their answer:

The memory leak is already fixed (.NET Framework 4.5 Developer Preview was the first publicly available build with the fix). As for the slowness, the Guid mismatch effectively deactivates our internal cache so we perform a bunch of registry lookups and by-name type lookups on each activation. Since this is not a mainline scenario, and should be limited to cases where the class has been re-Guid'ed without updating the registration, we are deprioritizing this issue for now. It's still tracked for future releases.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文