Net优化之字典性能优化

使用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:仅需一次哈希计算获取引用,后续直接操作内存,无额外开销。
  • TryGetValueTryGetValuedict[key] = value 分别计算哈希,导致两倍性能损耗。

适用场景

  • 高频更新场景GetValueRefOrNullRef 性能优势显著,适合需要频繁修改值的场景(如实时数据处理)。
  • 低并发场景TryGetValue 代码更直观,适合单线程或低频率操作。

结果预测:GetValueRefOrNullRef:预期耗时更低,尤其在大型字典中优势更明显(减少哈希计算)。

测试结果:

从上可以看出,GetValueRefOrNullRef方法在四个字典类型中都是领先TryGetValue方法的。

相关推荐
翻滚吧键盘26 分钟前
Spring Boot,两种配置文件
java·spring boot·后端
GoGeekBaird6 小时前
69天探索操作系统-第66天:为现代操作系统设计高级实时进程间通信机制
后端·操作系统
还是鼠鼠7 小时前
单元测试-概述&入门
java·开发语言·后端·单元测试·log4j·maven
我最厉害。,。8 小时前
接口安全&SOAP&OpenAPI&RESTful&分类特征导入&项目联动检测
后端·restful
AntBlack10 小时前
计算机视觉 : 端午无事 ,图像处理入门案例一文速通
后端·python·计算机视觉
福大大架构师每日一题11 小时前
2025-06-02:最小可整除数位乘积Ⅱ。用go语言,给定一个表示正整数的字符串 num 和一个整数 t。 定义:如果一个整数的每一位都不是 0,则称该整数为
后端
Code_Artist11 小时前
[Mybatis] 因 0 != null and 0 != '' 酿成的事故,害得我又过点啦!
java·后端·mybatis
程序员博博11 小时前
看到这种代码,我直接气到想打人
后端
南雨北斗12 小时前
php 图片压缩函数
后端
L2ncE12 小时前
ES101系列08 | 数据建模和索引重建
java·后端·elasticsearch