DependentHandle Impacts Performance
01/11/2026 | 6 minutes to read
DependentHandle is a special type of handle provided by the .NET Garbage Collector (GC).
This handle creates a dependency between the lifetime of two objects. It has a 'weak' reference to a target object and a reference to a dependent object. The weak reference to the target object means that a dependent handle does not extend the lifetime of this target object. The reference to the dependent object remains live as long as the target object is live.
Use-Case
A common use case for DependentHandle is when you need to associate additional fields with an object without extending its lifetime through a strong reference. While this type is not commonly used in typical line-of-business (LOB) applications, it is a handy tool for debuggers, profilers, and other diagnostic or development-time features. A special ConditionalWeakTable also exists, allowing a collection of DependentHandles to be stored in memory.
Usage
At the time of writing (.NET 9) a DependentHandle is not created using the typical GCHandle.Alloc(object? value, System.Runtime.InteropServices.GCHandleType type) method because the GCHandleType does not expose a corresponding enum value. Instead, a DependentHandle struct can be created using its constructor.
Here’s an example of creating a DependentHandle:
var target = new(); var dependent = new object(); var handle = new DependentHandle(target, dependent);
This links the lifetime of the dependent object to the lifetime of the target object.
Performance
While DependentHandle allows you to extend a data type with additional fields without modifying the source of the target object's type, it comes with significant performance implications. In this post, I create dependent handles with three different types of target objects. I measure the performance impact on the GC for each use-case. For this investigation, I use PerfView to measure GC pause durations.
No Dependent Handles
Let's first describe the benchmark with an example that does not use DependentHandles. It still uses the same number of objects, so this measurement provides a good baseline for comparison. The InitObj method creates an _object instance, which acts as a root object kept alive for the lifetime of the application. Then, it creates Size (1_000_000) number of objects stored in a rooted array.
public void InitObj() { _object = new(); for (int i = 0; i < Size; i++) _nohandles[i] = new(); } public object Run() { var stuff = new int[15000]; return stuff; }
The Run method is invoked 100_000 times during measurement. Each time, it allocates on the small object heap an array, that is then thrown away as garbage. This eventually triggers garbage collections, which are observed.
Baseline Results:
97 GCs (94 Gen0, 2 Gen1, 1 Gen2)
Total GC Pause: 39.5 msec
% Time paused for Garbage Collection: 37.4%
% CPU Time spent Garbage Collecting: 34.2%
Total Allocs : 604.497 MB
Most Gen0 collections complete in under 0.1 milliseconds, making this a highly efficient baseline.
Dependent on Root
In this test, the InitObj method is replaced with the InitDependentHandle method. This method creates the same number of dependent objects (note, that DependentHandle is a struct) as the previous example:
public void InitDependentHandle() { _object = new(); _handles[0] = new DependentHandle(_object, new object()); for (int i = 1; i < Size; i++) // 1_000_000 _handles[i] = new DependentHandle(_object, new object()); }
The DependentHandle instances are stored in a rooted array.
Performance results after executing the Run method:
95 GCs (94 Gen0, 0 Gen1, 1 Gen2)
Total GC Pause: 2,055.0 msec
% Time paused for Garbage Collection: 95.7%
% CPU Time spent Garbage Collecting: 94.1%
Total Allocs : 618.335 MB
This configuration spends significantly more time in the GC, with 94% of CPU time consumed by garbage collection — a concerning result. Most Gen0 collections take between 19-22 milliseconds, which is multiple times slower than the baseline.
Dependent on DependentHandle
In this case, the same number (1_000_000) of dependent handles are created, each targeting an already existing dependent handle:
_handles[i] = new DependentHandle(_handles[i-1], new object());
Results:
95 GCs (94 Gen0, 1 Gen1, 0 Gen2)
Total GC Pause: 595.3 msec
% Time paused for Garbage Collection: 90.0%
% CPU Time spent Garbage Collecting: 86.1%
Total Allocs : 600.431 MB
The results are like the previous test, with over 86% of CPU time still spent in garbage collection. The overall GC pause is over 15 times slower than the baseline. However, it is four times faster compared to the case where the dependent object is a managed object.
The usual Gen0 pause time is in the range of 4-6msec.
Dependent on Dependent object
In this case, a significantly smaller number (100_000) of dependent handles are created, each targeting the dependent object of a previously used dependent handle. Note that the GC requires special handling for these dependent handles. It must evaluate the dependent object to determine whether the current handle is still alive. For handle i, it checks if the DependentHandle[i-1]'s target object is live.
_handles[i] = new DependentHandle(_handles[i-1].Dependent, new object());
Results:
96 GCs (92 Gen0, 2 Gen1, 2 Gen2)
Total GC Pause: 36,271.5 msec
% Time paused for Garbage Collection: 99.9%
% CPU Time spent Garbage Collecting: 99.8%
Total Allocs : 598.975 MB
Nearly all CPU time is spent by the GC. The total pause time has ballooned to over 36 seconds with the same amount of allocations! The typical Gen0 pause times are ~0.2msec, however the two Gen2 and one Gen1 collections take over 10 seconds each. That is to look out for and avoid in a production application.
Conclusion
DependentHandles are a special type of handle in the .NET world that serve specific use cases like debuggers and profilers, rather than typical LOB applications. Through our performance investigation, we discovered several important characteristics:
Findings
Basic GC pause times increase dramatically when using
DependentHandles (15-900x slower)CPU time spent in GC ranges from 86% to 99.8% with different
DependentHandleconfigurationsChaining dependent handles (using dependent objects as targets) can lead to extreme GC pauses of over 10 seconds
The performance impact exists even with the same amount of allocated memory
While DependentHandles provide a unique way to link object lifetimes without extending the target object's lifetime, their performance implications make them unsuitable for high-performance scenarios or applications where GC pauses need to be minimized. If you need to use DependentHandles, careful consideration should be given to:
The number of handles created
The relationship between target and dependent objects
Avoiding chains of dependent handles
For most LOB applications, alternative designs that don't require DependentHandles should be considered first.