dotnet / dotnet/runtime

Huge GC performance degradation when many RCW are created

Open
#126,837 7 comments 3 reactions 0 assignees View on GitHub
area-Interop-coreclr tenet-performance
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

Our applications uses a lot of COM-like interfaces to communicate with C++ code. And I found that the GC became much slower when many of them created. To me it looks like even Gen 0 is affected.

I've attached small project that demonstrate the problem. [Demo.zip](https://github.com/user-attachments/files/26678397/Demo.zip)

It creates 1.5M RCW's and then simulates the Newtonsoft Deserialize function by creating strings and arrays. With 1.5M RCW's it takes more than 100 seconds on my PC. If you comment RCW creating (the first for-loop), then it takes from 2 to 2.5 seconds. Just in case creating RCW's takes only 1 second.

If you add following code:
```
public class ClientApiAgile : IClientApiAgile { }
```
and replace the body of the first for-loop to this:
```
rcws.Add(new ClientApiAgile());
```
it also takes 2.5 seconds, so number of objects is not a problem.

STR:
1. Open the C++ project from `Cpp` directory, switch to the Release configuration and compile project. I used VS 2022 to compile it. It has very basic COM-like interface.
2. Open the C# project from the `DotNet\App` directory, switch to the Release configuration and run it without debugger.

### Configuration

.NET 8, .NET 10.
Windows 11, x64

### Regression?
I don't think so.

### Data

* It takes 100 seconds to execute the application with 1.5M of RCW's
* When the first for loop is commented, it takes 2.5 seconds to execute the simulation.
* When the first loop creates 1.5M of the regular .NET objects, it takes 2.5 seconds to execute the simulation.

### Analysis

It looks like the GC calls `GCToEEInterface::AfterGcScanRoots`, that function calls `AppDomain::DetachRCWs` and that function calls `DetachWrappersWorker` where the most of the time is spent.

I understand that this code must run but perhaps it can execute less often. Or perhaps that code can execute faster.

We are porting some native code to C# but some bits and pieces are still on the native C++ side. We could have up to 5M of RCW's and I afraid that our customers will have micro-freezes when use our application.

From what I see it looks like there is no workaround. Probably the new COM that uses source generator can help, but we are using thousands interfaces and converting everything to the new COM will require a lot of work. Plus I think it also uses RCW's internally.

Any help or workaround will be greatly appreciated!

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.