.NET Core / .NET Standard标准库中,命名空间System.Collections.Generic
中,一些类是通过包System.Collections
分发的。本文分析了该包中不那么基础又有点令人混淆的SortedDictionary
、SortedList
和SortedSet
这三个类,并对有些相似的SortedList
和SortedSet
进行了一些性能对比。
System.Collections
包的源代码位于https://github.com/dotnet/runtime/tree/master/src/libraries/System.Collections。
IDictionary
以下几种类型存储的是键值对形式的数据,它们实现了IDictionary<TKey, TValue>接口。
SortedDictionary
源代码显示,SortedDictionary
的数据结构由基于红黑树的SortedSet
实现。其特性与SortedSet
一致。
SortedList
源代码显示,SortedList
使用两个线性表分别存储键和值。类成员数组keys
存储键,类成员数组values
存储值。
添加元素时,通过二分查找定位元素应插入的目标位置,并通过线性表的常规插入方法进行插入,复杂度为O(n)。若添加的元素本身已为升序,则复杂度为O(1)。插入时,若数组空间不足,则会按原数组的2倍长度或是所需要的数组大小(以较大的为准)创建新数组,并将原数据复制到新数组中,原数组由垃圾收集器按需回收。
查找元素时,使用二分查找,复杂度为O(log n)。
ICollection / ISet
以下的类型存储的是一般的对象,它们实现了ICollection<T>
接口。
SortedSet
源代码显示,SortedSet
的数据结构为红黑树。
SortedSet
通过树结构存储数据,插入和查找都具有O(log n)的时间复杂度。但其插入时需要在堆上分配内存,无法像SortedList
那样事先完成空间预留;插入有序数据时也无法提高性能。因为每个树节点所占的内存高于每个数组元素所占的内存,SortedSet
占用的内存以及查找时读取的内存比SortedList
要多。
通过IEnumerable
接口对SortedSet
进行遍历时,需要对红黑树进行中序遍历,因此它的枚举器(Enumerator
)中有一个用于存储遍历状态的栈,占用的内存大小与红黑树的深度成正比。
性能对比
SortedList
与SortedSet
的查找性能同样是O(log n),但它们的查找速度具体有何不同呢?用两种类型分别存储零到一百万的整数,并依次在集合中查找所有元素计算总时间,结果如下表所示,SortedList
比SortedSet
快50%。
类型 | 平均值 | 误差(置信度99.9%) | 标准差 |
---|---|---|---|
SortedList | 89.03 ms | 1.738 ms | 2.198 ms |
SortedSet | 134.07 ms | 0.913 ms | 0.763 ms |
SortedList
和SortedSet
查找所用时间SortedList
与SortedSet
的遍历过程速度有何不同呢?用两种类型分别存储零到一百万的整数,并对其进行foreach循环计算总时间,结果如下表所示,SortedList
比SortedSet
快110%。
类型 | 平均值 | 误差(置信度99.9%) | 标准差 |
---|---|---|---|
SortedList | 13.27 ms | 0.253 ms | 0.271 ms |
SortedSet | 27.92 ms | 0.383 ms | 0.320 ms |
SortedList
和SortedSet
遍历所用时间上述测试使用BenchmarkDotNet测得。测试环境:
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.17763.1282 (1809/October2018Update/Redstone5)
Intel Xeon Gold 5218 CPU 2.30GHz, 2 CPU, 64 logical and 32 physical cores
.NET Core SDK=3.1.100
[Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
测试代码:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
namespace MemoryTest1
{
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<SearchBenchmark>();
Console.ReadLine();
}
}
public class SearchBenchmark
{
private const int Size = 1000000;
private SortedList<int, int> sortedList;
private SortedSet<int> sortedSet;
public SearchBenchmark()
{
sortedList = new SortedList<int, int>();
sortedSet = new SortedSet<int>();
for (int i = 0; i < Size; ++i)
{
sortedList.Add(i, i);
sortedSet.Add(i);
}
}
[Benchmark]
public void SearchFromList()
{
for (int i = 0; i < Size; ++i)
{
sortedList.ContainsKey(i);
}
}
[Benchmark]
public void SearchFromSet()
{
for (int i = 0; i < Size; ++i)
{
sortedSet.Contains(i);
}
}
[Benchmark]
public void EnumerateList()
{
foreach (var item in sortedList)
{
}
}
[Benchmark]
public void EnumerateSet()
{
foreach (var item in sortedSet)
{
}
}
}
}
留言
有想法?请给我们留言!您的留言不会直接显示在网站内。