Description
Description
We have an Orleans cluster and in the silo, each grain depends on global configuration. Since that can change during runtime, we are using IOptionsMonitor
and when the grain is done, we dispose the handle we got from the OnChange
call. That last dispose call is the lone source of some hefty LOH allocations. The grains come and go constantly and at a certain load on the silo (around 2500 grains), LOH allocations start to happen.
As far as I can tell, IOptionsMonitor.Dispose
only deregisters from the internal multicast delegate and this deregistration is the main culprit: The multicast delegate allocates a new invocation list for each -=
, so memory consumption becomes O(n) while it's amortized O(1) for +=
due to exponential growth.
This small benchmark shows the problem. The allocations grow with the number of already registered delegates.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<Benchmark>();
[SimpleJob(iterationCount: 5)]
[MemoryDiagnoser]
public class Benchmark
{
private Action? _methods;
[Params(8, 10, 12, 14)]
public int Power { get; set; }
[GlobalSetup]
public void Setup()
{
_methods += Method;
for (var i = 0; i < Power; ++i)
{
_methods += _methods;
}
}
[Benchmark]
public void AddRemove()
{
var m = new Action(Method);
_methods += m;
_methods -= m;
}
private void Method() { }
}
| Method | Power | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|---------- |------ |-----------:|-----------:|-----------:|---------:|---------:|---------:|----------:|
| AddRemove | 8 | 1.512 us | 0.8605 us | 0.2235 us | 0.7629 | 0.0172 | - | 6.23 KB |
| AddRemove | 10 | 5.376 us | 2.8169 us | 0.4359 us | 2.9602 | 0.1831 | - | 24.23 KB |
| AddRemove | 12 | 18.840 us | 1.0445 us | 0.1616 us | 11.7493 | 1.4648 | - | 96.23 KB |
| AddRemove | 14 | 135.817 us | 74.9703 us | 19.4695 us | 124.8779 | 124.8779 | 124.8779 | 384.28 KB |