C#高级开发人员的性能指南

作者:微信公众号:【架构师老卢】
9-8 18:27
10

概述:在软件开发中,性能不仅仅是一种奢侈品,更是一种必需品。作为高级开发人员,我们经常面临为我们的任务选择最合适的数据结构的关键决策。List**T**、DictionaryTKey、TValue 和 HashSetT 之间的选择可以决定应用程序的性能,尤其是在大规模运行时。本文旨在为您提供做出明智决策所需的见解,并以性能指标和实际场景为后盾我们将探讨何时使用每种 Token、它们的性能特征以及如何优化它们以实现最高效率LISTT想象一下一把瑞士军刀。多才多艺,对吧?这就是我们的清单!当您需要所有东西时,它是首选工具。非常适合需要有序集合和频繁位置访问的方案它有什么好处?按特定顺序存储项集合从任何位

在软件开发中,性能不仅仅是一种奢侈品,更是一种必需品。作为高级开发人员,我们经常面临为我们的任务选择最合适的数据结构的关键决策。List**<T>**、Dictionary<TKey、TValue>HashSet<T> 之间的选择可以决定应用程序的性能,尤其是在大规模运行时。本文旨在为您提供做出明智决策所需的见解,并以性能指标和实际场景为后盾

我们将探讨何时使用每种 Token、它们的性能特征以及如何优化它们以实现最高效率

LIST<T>

想象一下一把瑞士军刀。多才多艺,对吧?这就是我们的清单!当您需要所有东西时,它是首选工具。

非常适合需要有序集合和频繁位置访问的方案

它有什么好处?

  • 按特定顺序存储项集合
  • 从任何位置添加或删除项目
  • 按索引访问项目

实际应用:待办事项列表应用程序

考虑一个顺序至关重要的任务管理系统:

public class Task
{
    public string Description { get; set; }
    public bool IsCompleted { get; set; }
}

public class ToDoList
{
    private List<Task> tasks = new List<Task>();

    public void AddTask(string description)
    {
        tasks.Add(new Task { Description = description, IsCompleted = false });
    }

    public void CompleteTask(int index)
    {
        if (index >= 0 && index < tasks.Count)
        {
            tasks[index].IsCompleted = true;
        }
    }

    public List<Task> GetIncompleteTasks()
    {
        return tasks.Where(t => !t.IsCompleted).ToList();
    }
}

在后台,List 使用动态数组。它就像一个神奇的扩展手提箱——它会随着您添加更多物品而增长。但这是关键所在!

当它增长时,它不仅会再增加一个槽位。它的大小翻了一番! 💨

这意味着添加项目通常非常快 (O(1)),但有时当它需要增长时,需要更长的时间 (O(n))。不过,平均而言,它仍然相当活泼!

那么,何时使用 List<T>?

  • 当您需要维护秩序时
  • 当您经常按位置访问项目时
  • 当您处理经常更改大小的集合时

以及,何时应该三思而后行?

  • 如果您经常按值(而不是索引)搜索项目
  • 如果您正在处理数百万件物品并需要闪电般快速的查找!!

Dictionary<TKey, TValue>

🪶 优化查找速度

当基于键的快速检索成为优先事项时,Dictionary<TKey, TValue> 成为一种强大的解决方案。

需要找点东西吗?干掉!你立刻就到了。🌠

它有什么好处?

  • 按键快速查找
  • 存储键值对
  • 检查键是否存在而不遍历所有内容

实际应用:用户配置文件缓存

在高流量应用程序中,高效的用户配置文件检索至关重要。假设我们正在为我们的社交媒体应用程序构建一个用户档案系统。我们希望通过用户的 ID 以超快的速度访问用户资料。

public class UserProfile
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
}

public class UserProfileCache
{
    private Dictionary<int, UserProfile> profileCache = new Dictionary<int, UserProfile>();

    public void AddOrUpdateProfile(UserProfile profile)
    {
        profileCache[profile.Id] = profile;
    }

    public UserProfile GetProfile(int userId)
    {
        return profileCache.TryGetValue(userId, out var profile) ? profile : null;
    }

    public bool ProfileExists(int userId)
    {
        return profileCache.ContainsKey(userId);
    }
}

Dictionary<TKey, TValue> 在后台使用哈希表。可以把它想象成一个超级有序的图书馆,其中每本书 (value) 都有一个唯一的索书号 (key)。图书管理员(哈希函数)几乎可以立即告诉您在哪里可以找到任何书籍!

为基于键的操作启用 O(1) 平均情况时间复杂度

那么,何时使用 Dictionary

  • 当您需要快速查找时
  • 当您使用键值对时
  • 当您需要快速检查是否存在时

以及,何时三思而后行

  • 如果您需要保持特定的顺序(请改用 SortedDictionary<TKey、TValue>)
  • 如果您正在处理少量项目(开销可能不值得)

HashSet<T>

🪶 高效的唯一性和集合操作

想象一下,在一家夜总会里,每个人都必须是独一无二的。这就是 HashSet。它是确保没有重复项进入的保镖。🚫👥

它在维护唯一元素集合至关重要的情况下表现出色

它有什么好处?

  • 存储唯一项目的集合
  • 超快检查会员资格
  • 设置并集、交集和差集等操作

实际应用:唯一标识符跟踪

让我们构建一个系统来跟踪我们的社交媒体应用程序中使用的独特主题标签。

public class HashtagTracker
{
    private HashSet<string> uniqueHashtags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    public bool AddHashtag(string hashtag)
    {
        return uniqueHashtags.Add(hashtag);
    }

    public bool HasBeenUsed(string hashtag)
    {
        return uniqueHashtags.Contains(hashtag);
    }

    public int UniqueHashtagCount => uniqueHashtags.Count;

    public IEnumerable<string> GetCommonHashtags(HashtagTracker other)
    {
        return uniqueHashtags.Intersect(other.uniqueHashtags, StringComparer.OrdinalIgnoreCase);
    }
}

HashSet 的构建原理与 Dictionary<TKey、TValue>相同,但它只存储键,不存储值

那么,何时使用 HashSet

  • 当您需要确保唯一性时
  • 当您需要快速成员资格测试
  • 当您执行 set 操作时

以及,何时三思而后行

  • 如果您需要将值与您的项目相关联(使用 Dictionary<TKey, TValue>)
  • 如果 order 很重要(考虑 SortedSet)

:: 性能分析 ::

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;

[MemoryDiagnoser]
public class DataStructureBenchmarks
{
    private const int N = 1_000_000;
    private List<int> _list;
    private Dictionary<int, bool> _dict;
    private HashSet<int> _set;

    [GlobalSetup]
    public void Setup()
    {
        _list = new List<int>(N);
        _dict = new Dictionary<int, bool>(N);
        _set = new HashSet<int>(N);

        var random = new Random(42);
        for (int i = 0; i < N; i++)
        {
            var value = random.Next();
            _list.Add(value);
            _dict[value] = true;
            _set.Add(value);
        }
    }

    [Benchmark]
    public bool ListContains() => _list.Contains(N / 2);

    [Benchmark]
    public bool DictContains() => _dict.ContainsKey(N / 2);

    [Benchmark]
    public bool SetContains() => _set.Contains(N / 2);

    [Benchmark]
    public void ListAdd() => _list.Add(N + 1);

    [Benchmark]
    public void DictAdd() => _dict[N + 1] = true;

    [Benchmark]
    public void SetAdd() => _set.Add(N + 1);
}

public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<DataStructureBenchmarks>();
    }
}
|      Method  |            Mean |         Error |        StdDev |   Gen 0 | Allocated |  
|------------  |----------------:|--------------:|--------------:|--------:|----------:|  
| ListContains | 2,924,483.95 ns | 57,638.858 ns | 91,235.298 ns |       - |      40 B |  
| DictContains |        22.36 ns |      0.479 ns |      0.787 ns |       - |         - |  
|  SetContains |        19.84 ns |      0.424 ns |      0.635 ns |       - |         - |  
|     ListAdd  |        27.59 ns |      0.589 ns |      0.918 ns |  0.0153 |      32 B |  
|     DictAdd  |        31.84 ns |      0.681 ns |      1.061 ns |  0.0153 |      32 B |  
|      SetAdd  |        31.47 ns |      0.673 ns |      1.048 ns |  0.0153 |      32 B |  
 

分析

对于 Lookup 操作Dictionary<TKey、TValue> 和 HashSet<T> 表现出卓越的性能,时间复杂度几乎恒定。List<T> 表现出线性的时间复杂度,使其不太适合大规模查找操作。

**对于 Insertion Operations (插入操作),**所有三种结构在单次插入时都显示出相当的性能。但是,由于内部数组大小调整,List<T> 可能会偶尔遇到性能影响。

对于内存利用率List<T> 被证明是每个元素的内存效率最高的。Dictionary<TKey、TValue> 和 HashSet<T> 用一些内存效率换取了它们在查找操作中的速度优势。

好吧,帅哥们!🔥 您已经掌握了基础知识,现在让我们通过一些高级技术将其提升一个档次

高级技术:微调以实现最佳性能

  1. 自定义相等比较器:您自己的比较逻辑

有时,您需要 Dictionary<TKey>TValue 或 HashSet 以特殊方式比较事物。因此,请实现 IEqualityComparer<T> 来定义自定义相等性和哈希代码生成:

public class CaseInsensitiveComparer : IEqualityComparer<string>
{
    public bool Equals(string x, string y) => string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
    public int GetHashCode(string obj) => obj.ToLowerInvariant().GetHashCode();
}

var uniqueStrings = new HashSet<string>(new CaseInsensitiveComparer());
uniqueStrings.Add("CSharp");
Console.WriteLine(uniqueStrings.Contains("csharp")); // True

2. 容量规划:优化内存分配

当您知道要添加大量项目时,请提前提醒您的数据结构:

var bigList = new List<int>(1_000_000); // Preallocate for better performance
var bigDict = new Dictionary<int, string>(1_000_000);

此策略可最大程度地减少重新分配开销,尤其适用于大型集合。

3. ConcurrentDictionary<TKey, TValue>:线程安全操作

对于多线程方案

private ConcurrentDictionary<int, UserProfile> _userCache = new ConcurrentDictionary<int, UserProfile>();

public UserProfile GetOrAddUser(int userId, Func<int, UserProfile> createProfile)
{
    return _userCache.GetOrAdd(userId, createProfile);
}

让我们案例研究: 构建高性能社交媒体后端

让我们运用我们的知识为社交媒体平台设计一个高性能的后端:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public HashSet<int> Followers { get; set; } = new HashSet<int>();
    public HashSet<int> Following { get; set; } = new HashSet<int>();
}

public class Post
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string Content { get; set; }
    public DateTime Timestamp { get; set; }
}

public class SocialMediaBackend
{
    private Dictionary<int, User> _users = new Dictionary<int, User>();
    private List<Post> _posts = new List<Post>();
    private Dictionary<string, HashSet<int>> _hashtags = new Dictionary<string, HashSet<int>>(StringComparer.OrdinalIgnoreCase);

    public void AddUser(User user)
    {
        _users[user.Id] = user;
    }

    public void AddPost(Post post)
    {
        _posts.Add(post);
        // Extract and index hashtags
        foreach (var word in post.Content.Split())
        {
            if (word.StartsWith("#"))
            {
                if (!_hashtags.TryGetValue(word, out var postIds))
                {
                    postIds = new HashSet<int>();
                    _hashtags[word] = postIds;
                }
                postIds.Add(post.Id);
            }
        }
    }

    public IEnumerable<Post> GetUserFeed(int userId)
    {
        if (_users.TryGetValue(userId, out var user))
        {
            return _posts.Where(p => user.Following.Contains(p.UserId))
                         .OrderByDescending(p => p.Timestamp);
        }
        return Enumerable.Empty<Post>();
    }

    public IEnumerable<Post> GetPostsByHashtag(string hashtag)
    {
        if (_hashtags.TryGetValue(hashtag, out var postIds))
        {
            return _posts.Where(p => postIds.Contains(p.Id))
                         .OrderByDescending(p => p.Timestamp);
        }
        return Enumerable.Empty<Post>();
    }
}
  • Dictionary<TKey, TValue> 用于快速查找用户和主题标签
  • HashSet<T> 用于高效的关注者/关注关系和主题标签-帖子关联
  • List<T> 用于维护帖子顺序并促进高效排序

数据结构的选择会显著影响应用程序的性能和可伸缩性。作为高级开发人员,我们的责任是根据每个场景的具体要求和约束做出明智的决策。

  • 当顺序很重要并且需要频繁的位置访问时>请使用 List<T。
  • 选择 Dictionary<TKey, TValue>此时基于键的快速查找至关重要。
  • 选择 HashSet<T> 以维护唯一的集合并执行集合操作。
阅读排行