none
Memory leak and huge performance degradation when creating instance of a .NET COM object without a GuidAttribute RRS feed

  • Question

  • 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.

    Now for the interesting part, leading up to my question. Lets 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 400MB.

    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 clearly points to the assembly and its codebase, and the type within the assembly.

    We also discovered that altering the Guid to something 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 scenario from a discovered memory leak in our product.

    Sincerely,
    Peter


    Friday, February 17, 2012 8:32 PM

Answers

  • I got the following reply from my bug report on connect, thought I'd share it here in case someone else encounters this problem;

    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. Please let me know if this resolution is not acceptable for you. Again, thanks for taking time to report the bug, it's very helpful.

    • Marked as answer by Peter Palotas Wednesday, April 4, 2012 2:31 PM
    Wednesday, April 4, 2012 2:31 PM

All replies

  • Peter,

    I wouldn't consider this a "but in .NET", as it only occurs when you do something improperly (ie: use a different GUID or no GUID, which isn't valid for COM) -

    That being said, I wonder if there's an issue with the way .NET is handling IUnknown::Release.  Have you checked the return value from Release() in this case?  It's possible that you're getting an extra reference count somehow when you create the object from the wrong GUID....

     


    Reed Copsey, Jr. - http://reedcopsey.com
    If a post answers your question, please click "Mark As Answer" on that post and "Mark as Helpful".

    Friday, February 17, 2012 9:02 PM
    Moderator
  • The refcount from the call to Release() returns 0, so that does not appear to be the problem.

    Also I haven't found any documentation that actually states that the GuidAttribute is required on an object that is to be exposed as a COM object, nor do I see any reason why it would have to be. Its only use that I can see is to provide information for the actual registration of the COM visible type, i.e. as used by RegAsm.exe.  Why is this attribute required for the instantiation of the object through COM?  And if indeed it is required, why doesn't CoCreateInstance return an error instead of a producing an incredibly slow response followed by a memory leak?



    Friday, February 17, 2012 9:17 PM
  • I got the following reply from my bug report on connect, thought I'd share it here in case someone else encounters this problem;

    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. Please let me know if this resolution is not acceptable for you. Again, thanks for taking time to report the bug, it's very helpful.

    • Marked as answer by Peter Palotas Wednesday, April 4, 2012 2:31 PM
    Wednesday, April 4, 2012 2:31 PM