使用CollectionsMarshal优化字典的处理性能
从字典中获取或更新的方法可以这样写:
csharp
public static TValue? GetOrUpdate<TKey, TValue>(this Dictionary<TKey, TValue> dict,
TKey key, TValue value)
{
if (dict.TryGetValue(key, out var val))
{
dict[key] = value;
return val;
}
return default;
}
我们可以尝试引入CollectionsMarshal方法以少量的修改代价提高方法的性能。CollectionsMarshal 是 .NET 5+ 引入的一个工具类,位于 System.Runtime.InteropServices
命名空间。它提供了一系列 底层操作集合的方法,旨在绕过集合的抽象层以提升性能,适用于对性能敏感的特定场景。该类主要用于直接访问集合的内部数据结构(如数组),避免常规操作中的冗余检查。
csharp
public static TValue? GetOrUpdate<TKey, TValue>(this Dictionary<TKey, TValue> dict,
TKey key, TValue value)
{
ref var val = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref val))
{
val = value;
}
return val;
}
CollectionsMarshal 提供了一些高级方法,用于直接操作集合的内部数据,适用于需要高性能和低开销的场景。然而,由于这些方法绕过了集合的正常保护机制,使用时需要格外小心,确保代码的正确性和安全性
进行基准测试:
csharp
public class Class1
{
public Guid Id { get; set; }
}
public class ClassBig
{
public Guid Id { get; set; }
public Guid Id2 { get; set; }
public Guid Id3 { get; set; }
public Guid Id4 { get; set; }
public Guid Id5 { get; set; }
public Guid Id6 { get; set; }
public Guid Id7 { get; set; }
public Guid Id8 { get; set; }
public Guid Id9 { get; set; }
public Guid Id10 { get; set; }
public Guid Id11 { get; set; }
}
[MemoryDiagnoser(false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[SimpleJob(RuntimeMoniker.Net80)]
public class DicBenchmark
{
private readonly Guid _existId = Guid.NewGuid();
private readonly string _existIds;
private readonly Dictionary<Guid, string> _stringValDict;
private readonly Dictionary<string, Guid> _stringKeyDict;
private readonly Dictionary<Guid, Class1> _class1ValDict;
private readonly Dictionary<string, ClassBig> _classBigValDict;
public DicBenchmark()
{
_existIds = _existId.ToString();
_stringValDict = new Dictionary<Guid, string>()
{
{_existId,_existId.ToString() }
};
_stringKeyDict = new Dictionary<string, Guid>()
{
{_existId.ToString(),_existId }
};
_classBigValDict = new Dictionary<string, ClassBig>()
{
{_existId.ToString(),new ClassBig{Id=_existId} }
};
_class1ValDict = new Dictionary<Guid, Class1>
{
{_existId,new Class1{Id=_existId} }
};
}
[BenchmarkCategory("StringVal")]
[Benchmark]
public string GetValueRefOrNullRef_UpdateExist_StringVal()
{
ref var val = ref CollectionsMarshal.GetValueRefOrNullRef(_stringValDict, _existId);
if (!Unsafe.IsNullRef(ref val))
{
val = _existId.ToString();
}
return val;
}
[BenchmarkCategory("StringVal")]
[Benchmark]
public string TryGetValue_UpdateExist_StringVal()
{
if (_stringValDict.TryGetValue(_existId, out var val))
{
_stringValDict[_existId] = _existId.ToString();
return val;
}
return string.Empty;
}
[BenchmarkCategory("StringKey")]
[Benchmark]
public Guid GetValueRefOrNullRef_UpdateExist_StringKey()
{
ref var val = ref CollectionsMarshal.GetValueRefOrNullRef(_stringKeyDict, _existIds);
if (!Unsafe.IsNullRef(ref val))
{
val = _existId;
}
return val;
}
[BenchmarkCategory("StringKey")]
[Benchmark]
public Guid TryGetValue_UpdateExist_StringKey()
{
if (_stringKeyDict.TryGetValue(_existIds, out var val))
{
_stringKeyDict[_existIds] = _existId;
return val;
}
return Guid.Empty;
}
[BenchmarkCategory("ClassVal")]
[Benchmark]
public Class1 GetValueRefOrNullRef_UpdateExist_ClassVal()
{
ref var val = ref CollectionsMarshal.GetValueRefOrNullRef(_class1ValDict, _existId);
if (!Unsafe.IsNullRef(ref val))
{
val = new Class1 { Id=_existId};
}
return val;
}
[BenchmarkCategory("ClassVal")]
[Benchmark]
public Class1 TryGetValue_UpdateExist_ClassVal()
{
if (_class1ValDict.TryGetValue(_existId, out var val))
{
_class1ValDict[_existId] = new Class1 { Id = _existId };
return val;
}
return new Class1();
}
[BenchmarkCategory("ClassBigVal")]
[Benchmark]
public ClassBig GetValueRefOrNullRef_UpdateExist_ClassBigVal()
{
ref var val = ref CollectionsMarshal.GetValueRefOrNullRef(_classBigValDict, _existIds);
if (!Unsafe.IsNullRef(ref val))
{
val = new ClassBig { Id= _existId };
}
return val;
}
[BenchmarkCategory("ClassBigVal")]
[Benchmark]
public ClassBig TryGetValue_UpdateExist_ClassBigVal()
{
if (_classBigValDict.TryGetValue(_existIds, out var val))
{
_classBigValDict[_existIds] = new ClassBig { Id = _existId };
return val;
}
return new ClassBig { Id = _existId };
}
}
GetValueRefOrNullRef的好处是:
- 避免哈希计算开销:通过引用直接操作字典内部数据结构,省去了
TryGetValue
的重复哈希查找。 - 内存操作高效:直接修改引用,减少临时变量的内存分配。
性能差异分析:
GetValueRefOrNullRef
:仅需一次哈希计算获取引用,后续直接操作内存,无额外开销。TryGetValue
:TryGetValue
和dict[key] = value
分别计算哈希,导致两倍性能损耗。
适用场景
- 高频更新场景 :
GetValueRefOrNullRef
性能优势显著,适合需要频繁修改值的场景(如实时数据处理)。 - 低并发场景 :
TryGetValue
代码更直观,适合单线程或低频率操作。
结果预测:GetValueRefOrNullRef
:预期耗时更低,尤其在大型字典中优势更明显(减少哈希计算)。
测试结果:
从上可以看出,GetValueRefOrNullRef方法在四个字典类型中都是领先TryGetValue方法的。