I've just searched the net for information on MemoryCache
performance and stumbled upon this SO question. I asked myself why a proper benchmark library wasn't used, so I've ended up cooking my own benchmark by being very lazy (as all good programmers should :-) and used the incredible BenchmarkDotNet library to check how well (or not) this class behaves.
First the results
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
Frequency=1757813 Hz, Resolution=568.8887 ns, Timer=TSC
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0
DefaultJob : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0
| Method | N | Mean | Error | StdDev |
|------------------------------- |------ |----------:|----------:|----------:|
| FindDadosEmpInCache | 30000 | 231.40 ns | 0.4435 ns | 0.3703 ns |
| FindDataAtTheEnd | 30000 | 429.90 ns | 1.1490 ns | 1.0186 ns |
| FindDataInDictionary | 30000 | 24.09 ns | 0.2244 ns | 0.2099 ns |
| FindDataInConcurrentDictionary | 30000 | 29.66 ns | 0.0990 ns | 0.0926 ns |
| FindDataInHashset | 30000 | 16.25 ns | 0.0077 ns | 0.0065 ns |
Now some explaining...
I was mostly interested in seeing how fast MemoryCache
would compare to hashed lists (Dictionary
, Hashset
...) with thousands of entries and also to a worst case linear search over such "long" list. So I've added some additional tests and realized that while MemoryCache
is not as fast as the simple or concurrent lists, the speed still lies at the nanosecond scale. Not even a single millisecond is taken to retrieve an item in a 30,000 long list of cached items.
To be fair MemoryCache
does a LOT more than those simple lists as it must control concurrency, item expiration/eviction, etc. I believe it is fast enough for all kinds of workloads, but if you don't need its added features, like eviction policies, you should better stick with the simpler hashed lists implementations.
On the other hand, since it's an order a magnitude "slower" than a hash lookup, there may be room for improvement. I guess the designers thought it is just good enough as it is, and who am I to disagree with the DOTNET engineers? :-)
Here is the source code for the benchmark program:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
namespace TestListPerformance
{
class Program
{
static void Main(string[] args) {
var summary = BenchmarkRunner.Run<BenchmarkMemoryCache>();
}
}
public class BenchmarkMemoryCache
{
[Params(30000)]
public int N { get; set; }
public string FindStr;
private IList<DadosEmp> data;
private Dictionary<string, DadosEmp> dict;
private ConcurrentDictionary<string, DadosEmp> concurrentDict;
private HashSet<DadosEmp> hashset;
private DadosEmp last;
[GlobalSetup]
public void BuildData() {
FindStr = N.ToString();
data = new List<DadosEmp>(N);
dict = new Dictionary<string, DadosEmp>(N);
concurrentDict = new ConcurrentDictionary<string, DadosEmp>();
hashset = new HashSet<DadosEmp>();
for (int i = 0; i <= N; i++) {
DadosEmp d;
data.Add(d = new DadosEmp {
Identificacao = i,
Pis = i * 100,
NumCartao = i * 1000,
Nome = "Nome " + i.ToString(),
});
MemoryCache.Default.Add(i.ToString(), d,
new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(30) });
dict.Add(i.ToString(), d);
concurrentDict.TryAdd(i.ToString(), d);
hashset.Add(d);
last = d;
}
}
[Benchmark]
public DadosEmp FindDadosEmpInCache() {
var f = (DadosEmp)MemoryCache.Default.Get(FindStr);
return f;
}
[Benchmark]
public DadosEmp FindDataAtTheEnd() {
var f = data.FirstOrDefault(e => e.NumCartao == N || e.Pis == N || e.Identificacao == N);
return f;
}
[Benchmark]
public DadosEmp FindDataInDictionary() {
var f = dict[FindStr];
return f;
}
[Benchmark]
public DadosEmp FindDataInConcurrentDictionary() {
var f = concurrentDict[FindStr];
return f;
}
[Benchmark]
public bool FindDataInHashset() {
return hashset.Contains(last);
}
}
public class DadosEmp : IEquatable<DadosEmp>
{
public const string BIO_EXCLUSAO = "xbio";
public DadosEmp() {
Biometrias = new List<string>();
}
public long Identificacao { get; set; }
public long Pis { get; set; }
public long NumCartao { get; set; }
public string Nome { get; set; }
public int VersaoBio { get; set; }
public string Unidade { get; set; }
public IList<string> Biometrias { get; set; }
public string Biometria { get; set; }
public bool ExcluirBiometria { get { return Biometria == BIO_EXCLUSAO; } }
public DateTime DataEnvioRep { get; set; }
public string SenhaTeclado { get; set; }
public bool ExigeAutorizacaoSaida { get; set; }
public bool BioRepPendente { get; set; }
public override bool Equals(object obj) {
DadosEmp e = obj as DadosEmp;
if (ReferenceEquals(e, null))
return false;
return Equals(e);
}
public bool Equals(DadosEmp e) {
if (ReferenceEquals(e, null))
return false;
return e.Pis == this.Pis;
}
public override int GetHashCode() {
return Pis.GetHashCode();
}
public override string ToString() {
return string.Format("{0} ({1} - {2})", Nome, Pis, Identificacao);
}
}
}