# DependentHandle Impacts Performance

[DependentHandle](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.dependenthandle?view=net-9.0) 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](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2?view=net-9.0) also exists, allowing a collection of `DependentHandle`s 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](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandletype?view=net-9.0) 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`:

```csharp
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 `DependentHandle`s. 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.

```csharp
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:

```csharp
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:

```csharp
_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.

```csharp
_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

`DependentHandle`s 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 `DependentHandle`s (15-900x slower)
- CPU time spent in GC ranges from 86% to 99.8% with different `DependentHandle` configurations
- Chaining 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 `DependentHandle`s 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 `DependentHandle`s, 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 `DependentHandle`s should be considered first.