Skip to content

Commit 043b548

Browse files
authored
Optimize dispose logic (#48)
Provide a generic Disposer class to centralize dispose logic for disposable items in caches. Verified this has equal performance and reduces generated code size.
1 parent d9981a2 commit 043b548

File tree

6 files changed

+511
-407
lines changed

6 files changed

+511
-407
lines changed

BitFaster.Caching.Benchmarks/BitFaster.Caching.Benchmarks.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
</PropertyGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
17+
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
1818
<PackageReference Include="MathNet.Numerics" Version="4.11.0" />
19-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
20-
<PackageReference Include="System.Runtime.Caching" Version="4.7.0" />
19+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
20+
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
2121
</ItemGroup>
2222

2323
<ItemGroup>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.CompilerServices;
4+
using System.Text;
5+
using BenchmarkDotNet.Attributes;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.Diagnostics.Runtime.Interop;
8+
9+
namespace BitFaster.Caching.Benchmarks
10+
{
11+
// Is it possible to write a class to eliminate the dispose code for types that are not IDisposable?
12+
// https://github.com/dotnet/runtime/issues/4920
13+
[DisassemblyDiagnoser(printSource: true)]
14+
[MemoryDiagnoser]
15+
public class DisposerBench
16+
{
17+
[Benchmark(Baseline = true)]
18+
public void HandWritten()
19+
{
20+
for (int i = 0; i < 1000; i++)
21+
{
22+
NotDisposable notDisposable = new NotDisposable();
23+
Disposable disposable = new Disposable();
24+
disposable.Dispose();
25+
}
26+
}
27+
28+
[Benchmark()]
29+
public void NotOptimized()
30+
{
31+
for (int i = 0; i < 1000; i++)
32+
{
33+
NotDisposable notDisposable = new NotDisposable();
34+
Disposable disposable = new Disposable();
35+
36+
if (notDisposable is IDisposable)
37+
{
38+
((IDisposable)notDisposable).Dispose();
39+
}
40+
41+
if (disposable is IDisposable)
42+
{
43+
((IDisposable)disposable).Dispose();
44+
}
45+
}
46+
}
47+
48+
[Benchmark()]
49+
public void GenericDisposerReadonlyProperty()
50+
{
51+
for (int i = 0; i < 1000; i++)
52+
{
53+
NotDisposable notDisposable = new NotDisposable();
54+
Disposable disposable = new Disposable();
55+
Disposer<Disposable>.Dispose(disposable);
56+
Disposer<NotDisposable>.Dispose(notDisposable);
57+
}
58+
}
59+
60+
[Benchmark()]
61+
public void GenericDisposerStdCheck()
62+
{
63+
for (int i = 0; i < 1000; i++)
64+
{
65+
NotDisposable notDisposable = new NotDisposable();
66+
Disposable disposable = new Disposable();
67+
Disposer2<Disposable>.Dispose(disposable);
68+
Disposer2<NotDisposable>.Dispose(notDisposable);
69+
}
70+
}
71+
}
72+
73+
public static class Disposer<T>
74+
{
75+
// try using a static readonly field
76+
private static readonly bool shouldDispose = typeof(IDisposable).IsAssignableFrom(typeof(T));
77+
78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
79+
public static void Dispose(T value)
80+
{
81+
if (shouldDispose)
82+
{
83+
((IDisposable)value).Dispose();
84+
}
85+
}
86+
}
87+
88+
public static class Disposer2<T>
89+
{
90+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
91+
public static void Dispose(T value)
92+
{
93+
if (value is IDisposable d)
94+
{
95+
d.Dispose();
96+
}
97+
}
98+
}
99+
100+
public class NotDisposable
101+
{ }
102+
103+
public class Disposable : IDisposable
104+
{
105+
private bool isDisposed = false;
106+
107+
public void Dispose()
108+
{
109+
if (!isDisposed)
110+
this.isDisposed = true;
111+
}
112+
}
113+
}

BitFaster.Caching/Disposer.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace BitFaster.Caching
9+
{
10+
public static class Disposer<T>
11+
{
12+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
13+
public static void Dispose(T value)
14+
{
15+
if (value is IDisposable d)
16+
{
17+
d.Dispose();
18+
}
19+
}
20+
}
21+
}

BitFaster.Caching/Lru/ClassicLru.cs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ public V GetOrAdd(K key, Func<K, V> valueFactory)
104104
{
105105
dictionary.TryRemove(first.Value.Key, out var removed);
106106

107-
if (removed.Value.Value is IDisposable d)
108-
{
109-
d.Dispose();
110-
}
107+
Disposer<V>.Dispose(removed.Value.Value);
111108
}
112109

113110
return node.Value.Value;
@@ -151,10 +148,7 @@ public async Task<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
151148
{
152149
dictionary.TryRemove(first.Value.Key, out var removed);
153150

154-
if (removed.Value.Value is IDisposable d)
155-
{
156-
d.Dispose();
157-
}
151+
Disposer<V>.Dispose(removed.Value.Value);
158152
}
159153

160154
return node.Value.Value;
@@ -182,10 +176,7 @@ public bool TryRemove(K key)
182176
}
183177
}
184178

185-
if (node.Value.Value is IDisposable d)
186-
{
187-
d.Dispose();
188-
}
179+
Disposer<V>.Dispose(node.Value.Value);
189180

190181
return true;
191182
}
@@ -245,10 +236,7 @@ public void AddOrUpdate(K key, V value)
245236
{
246237
dictionary.TryRemove(first.Value.Key, out var removed);
247238

248-
if (removed.Value.Value is IDisposable d)
249-
{
250-
d.Dispose();
251-
}
239+
Disposer<V>.Dispose(removed.Value.Value);
252240
}
253241

254242
return;

0 commit comments

Comments
 (0)