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.