.NET中Dictionary<TKey, TValue>浅析
.NET中Dictionary<TKey, Tvalue>是非常常用的key-value的数据结构,也就是其实就是传说中的哈希表。.NET中还有一个叫做Hashtable的类型,两个类型都是哈希表。这两个类型都可以实现键值对存储的功能,区别就是一个是泛型一个不是并且内部实现有一些不同。今天就研究一下.NET中的Dictionary<TKey, TValue>以及一些相关问题。
guid:33b4b911-2068-4513-9d98-31b2dab4f70c
文中如有错误,望指出。
什么是哈希表
Wikipedia中对于Hash table的定义是这样的:
In computing, a hash table (also hash map) is a data structure used to implement an associative array, a structure that can map keys to values.
它是一个通过关键字直接访问内存存储位置的数据结构,这是一个所有数据结构教科书里都会有的一个数据结构,这里不做太多的研究。但是有几个概念还是要提一下,因为对于我们理解Dictionary的内部实现很大作用。
更多哈希表的内容:Wikipedia,Hashtable 博客
碰撞(Collision)及处理
由于我们在数据结构中采用的哈希算法不是一个完美的哈希算法,同时我们会限制我们用来存储的内存空间。所以发生碰撞是不可能避免的,所以很处理如何碰撞就是设计哈希表的时候需要考虑的一个很重要的因素。
处理碰撞有很多方法,例如开放寻址法(Open Adressing),分离链接法(Separate chaining)。Dictionary中采用的是一个叫做Separate chaining with linked lists的方法。
通过下面这个Wikipedia上的图就可以很清楚的认识到这是一个什么样子的方法。
利用两个数组,buckets数组只保存一个地址,这个地址指向的是entries数组中的一个实例(entry)。当哈希值冲突的时候则需要往当前指向的那个实例的链表的末端添加一个新实例即可。
装填因子(Load Factor)
装填因子的存在是因为在开放定址方法中,当数组中的内容越来越多的时候则冲突的概率就会越来越大,而在开放定址方法中冲突的解决方案是采用探测法,而这种冲突会带来性能的极大损失。wikipedia中的这张图比较了分散链接和线性探测法在不同装填因子的情况下CPU缓存不命中的对应关系。
在Dictionary采用的这种方法里,装填因子并不是一个重要的因素不会对性能有太大影响,所以Dictionary默认使用了1并认为没有必要提供任何接口去设置这个值。
Dictionary内部如何实现
先来介绍几个在Dictionary中重要的变量:
int[] buckets
和Entry[] entries
IEqualityComparer<TKey> comparer
.
- 这两个就是在上面提到的两个数组,所谓的 Separate chaining with linked lists。
- 在往Dictionary中添加一对新值的时候需要计算key的Hashcode,冲突的时候也需要判断两个value是不是相等。这个comparer就是来做这个事情的,那么这里为什么不直接调用key重载的GetHashCode和Equal方法呢?这个会在下文中讲到。
插入
通过一个例子来说明Dictionary在插入的时候做了什么。
- Dictionary<int, string=""> dict = new Dictionary<int, string="">();
- dict.Add(0, "zero");
- dict.Add(12, "twelve");
- dict.Add(15, "fiften");
- dict.Add(4, "four");
下面这张“图”能够看出两个数组在插入操作中的变化,结合源代码细细品味就能知道发生了什么。
- --------- ---------
- |buckets| |entries|
- |-------| |-------|
- | 0 | | -->| hashcode=0,key=0,next=-1,value="zero"
- |-------| |-------|
- | -1 | | empty |
- |-------| |-------|
- | -1 | | empty |
- |-------| |-------|
- --------- ---------
- |buckets| |entries|
- |-------| |-------|
- | 1 | | -->| hashcode=0,key=0,next=-1,value="zero"
- |-------| |-------|
- | -1 | | -->| hashcode=12,key=12,next=0,value="twelve"
- |-------| |-------|
- | -1 | | empty |
- |-------| |-------|
- --------- ---------
- |buckets| |entries|
- |-------| |-------|
- | 2 | | -->| hashcode=0,key=0,next=-1,value="zero"
- |-------| |-------|
- | -1 | | -->| hashcode=12,key=12,next=0,value="twelve"
- |-------| |-------|
- | -1 | | -->| hashcode=15,key=15,next=1,value="fiften"
- |-------| |-------|
- --------- ---------
- |buckets| |entries|
- |-------| |-------|
- | 0 | | -->| hashcode=0,key=0,next=-1,value="zero"
- |-------| |-------|
- | 2 | | -->| hashcode=12,key=12,next=-1,value="twelve"
- |-------| |-------|
- | -1 | | -->| hashcode=15,key=15,next=-1,value="fiften"
- |-------| |-------|
- | -1 | | -->| hashcode=4,key=4,next=-1,value="four"
- |-------| |-------|
- | 3 | | empty |
- |-------| |-------|
- | 1 | | empty |
- |-------| |-------|
- | -1 | | empty |
- |-------| |-------|
扩容
上面例子中最后一个插入时,Dictionary就做了一次扩容。它将Dictionary的size从原有的3扩展到了7。可以看到entries中的元素在扩容的时候变化不大,只是next有了些变化,这是因为扩容以后他们的哈希值%length不再映射到同一个值上了,也就不需要共享一个值啦。我觉得这里有两点值得一提。
- 扩容的下一个size是如何得到的,在上面的例子中为什么是7?
- 扩容是两个数组的变化。
对于第一个问题,因为Dictionary中数组长度有限,所以是通过key.GetHashCode() % length来获得一个bucket数组中的位置然后更改entry的next。那么我们就要保证尽可能的减少取模带来的冲突次数,那么素数就能够很好的保证取模以后能够尽可能的分散在数组的各处。
Dictionary扩容的时候会先把当前的容量*2,然后再在一个素数表中找到比这个值大的最近的一个素数。这个素数表是长这样子的:
- public static readonly int[] primes = {
- 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
- 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
- 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
- 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
- 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};
至于第二个问题,我们在上文中也提到过了,这个方法在扩容的时候是不需要对哈希表中存储的内容进行重新哈希。我们只需要将bucket中的分布的元素所对应的哈希值重新进行取模运算,然后放到新的位置上即可,这个操作是极快的。
查找
key.GetHashCode() % length --> 遍历链表找到equal的key
删除
同查找。
几个注意事项
性能问题
我们使用Dictionary的时候一般的习惯应该就跟代码1中那样,这种使用方法在我们使用内置的类型当key的时候没有问题,但是如果我们需要将一个自定义的值类型(struct)当作key的时候就需要注意了。这里有一个很容易忽略的问题,会导致使用Dictionary的时候带来大量不必要的性能开销。
当我们需要定义一些自定义结构并且要把这些实例放在集合中的时候我们往往会采用值类型而不会定义成一个类,如果这些类型只存在数据的话值类型在性能上比类要好很多。(Choosing Between Class and Struct)
我们先做一个实验来比较一下值类型和类作为key的性能有多大的差距。实验代码如下,这段代码中我插入1000个到10000个数据来得到所需要的时间。
- public class/struct CustomKey
- {
- public int Field1;
- public int Field2;
- public override int GetHashCode()
- {
- return Field1.GetHashCode() ^
- Field2.GetHashCode();
- }
- public override bool Equals(object obj)
- {
- CustomKey key = (CustomKey)obj;
- return this.Field1 == key.Field1 &&
- this.Field2 == key.Field2;
- }
- }
- Dictionary<CustomKey, int> dict = new Dictionary<CustomKey, int>();
- int tryCount = 50;
- double totalTime = 0.0;
- for (int count = 1000; count < 10000; count += 1000)
- {
- for (int j = 0; j < tryCount; j++)
- {
- Stopwatch watcher = Stopwatch.StartNew();
- for (int i = 0; i < count; i++)
- {
- CustomKey key = new CustomKey() { Field1 = i * 2, Field2 = i * 2 + 1 };
- dict.Add(key, i);
- }
- watcher.Stop();
- dict.Clear();
- totalTime += watcher.ElapsedMilliseconds;
- }
- Console.WriteLine("{0},{1}", count, totalTime / tryCount);
- }
结果是这样子的:
WTF?为什么和我的预期不一样,不是应该值类型要快才对不是么?orz....
这里就要提到刚刚在上文中提到的那个IEqualityComparer<TKey> comparer
,Dictioanry内部的比较都是通过这个实例来进行的。但是我们没有指定它,那么它使用的就是EqualityComparer<TKey>.Default
。让我们看一下源代码来了解一下这个Default到底是怎么来的,在CreateComparer
我们可以看到如果我们的类型不是byte
、没实现IEquatable<T>
接口、不是Nullable<T>
、不是enum
的话,会默认给我们创建一个ObjectEqualityComparer<T>()
。
而ObjectEqualityComparer<T>()
中的Equal和GetHashCode方法看上去也没啥问题,那到底问题出在哪里呢?
跟值类型有关的性能问题,马上能够想到的就是装箱和拆箱所带来的性能损耗。这里那里存在这种操作呢?我们来看一下下面的两段代码就明白了。
- ObjectEqualityComparer.Equals(T x, T y)的IL代码
- // Methods
- .method public hidebysig virtual
- instance bool Equals (
- !T x,
- !T y
- ) cil managed
- {
- // Method begins at RVA 0x62a39
- // Code size 50 (0x32)
- .maxstack 8
- IL_0000: ldarg.1
- IL_0001: box !T
- IL_0006: brfalse.s IL_0026
- IL_0008: ldarg.2
- IL_0009: box !T
- IL_000e: brfalse.s IL_0024
- IL_0010: ldarga.s x
- IL_0012: ldarg.2
- IL_0013: box !T
- IL_0018: constrained. !T
- IL_001e: callvirt instance bool System.Object::Equals(object)
- IL_0023: ret
- IL_0024: ldc.i4.0
- IL_0025: ret
- IL_0026: ldarg.2
- IL_0027: box !T
- IL_002c: brfalse.s IL_0030
- IL_002e: ldc.i4.0
- IL_002f: ret
- IL_0030: ldc.i4.1
- IL_0031: ret
- } // end of method ObjectEqualityComparer`1::Equals
- ObjectEqualityComparer.Equals(T x, T y)的IL代码
- .method public hidebysig virtual
- instance int32 GetHashCode (
- !T obj
- ) cil managed
- {
- .custom instance void System.Runtime.TargetedPatchingOptOutAttribute::.ctor(string) = (
- 01 00 3b 50 65 72 66 6f 72 6d 61 6e 63 65 20 63
- 72 69 74 69 63 61 6c 20 74 6f 20 69 6e 6c 69 6e
- 65 20 61 63 72 6f 73 73 20 4e 47 65 6e 20 69 6d
- 61 67 65 20 62 6f 75 6e 64 61 72 69 65 73 00 00
- )
- // Method begins at RVA 0x62a6c
- // Code size 24 (0x18)
- .maxstack 8
- IL_0000: ldarg.1
- IL_0001: box !T
- IL_0006: brtrue.s IL_000a
- IL_0008: ldc.i4.0
- IL_0009: ret
- IL_000a: ldarga.s obj
- IL_000c: constrained. !T
- IL_0012: callvirt instance int32 System.Object::GetHashCode()
- IL_0017: ret
- } // end of method ObjectEqualityComparer`1::GetHashCode
从上面两段代码中可以看到在ObjectEqualityComparer的默认实现中会存在着很多的box(见高亮行)操作,它是用来将值类型装箱成引用类型的。这个操作是很耗时的,因为它需要创建一个object并将值类型中的值拷贝到新创建的对象中。(CustomKey.Equal方法中也有一个unbox操作)。
怎么破?
我觉得只要避免装箱不就行了,那我们自己创建一个Comparer。
- public class MykeyComparer : IEqualityComparer
- {
- #region IEqualityComparer Members
- public bool Equals(CustomKey x, CustomKey y)
- {
- return x.Field1 == y.Field1 &&
- x.Field2 == y.Field2;
- }
- public int GetHashCode(CustomKey obj)
- {
- return obj.Field1.GetHashCode() ^
- obj.Field2.GetHashCode();
- }
- #endregion
- }
那我们将实验代码稍作修改(Dictionary<CustomKey, int> dict = new Dictionary<CustomKey, int>(new MykeyComparer());
)在测试一把。这次的结果显示性能提高了很多。
线程安全
这货不是线程安全的,需要多线程操作要么自己维护同步要么使用线程安全的Dictionary-->ConcurrentDictionary<TKey, TValue>
先到这里吧
.NET中Dictionary<TKey, TValue>浅析的更多相关文章
- C#中Dictionary<TKey,TValue>排序方式
自定义类: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sy ...
- C# .Net 中字典Dictionary<TKey,TValue>泛型类 学习浅谈
一.综述: Dictionary<TKey,TValue>是在 .NET Framework 2.0 版中是新增的.表示键值对的集合,Dictionary<TKey,TValue&g ...
- C#中数组、集合(ArrayList)、泛型集合List<T>、字典(dictionary<TKey,TValue>)全面对比
C#中数组.集合(ArrayList).泛型集合List<T>.字典(dictionary<TKey,TValue>)全面对比 为什么把这4个东西放在一起来说,因为c#中的这4 ...
- .net源码分析 – Dictionary<TKey, TValue>
接上篇:.net源码分析 – List<T> Dictionary<TKey, TValue>源码地址:https://github.com/dotnet/corefx/blo ...
- Dictionary<TKey, TValue> 类
C# Dictionary<TKey, TValue> 类 Dictionary<TKey, TValue> 泛型类提供了从一组键到一组值的映射.字典中的每个添加项都由一个值及 ...
- C# 字典 Dictionary<Tkey,Tvalue>
最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来.我们都知道计算机技术发展日新月异,速度惊人的快,你我稍不留神,就会被慢慢淘汰!因此:每日不间断的学习是避免被 ...
- 巧用Dictionary<TKey,TValue>,完成客户需求
前几天与客户沟通一个项目,客户对其中某个模块提了一个需求. 把从数据库中取出的对物品的统计重新拆分重新统计.鉴于用文字不能清除的表达需求,我将该需求画出来,便于理解. 需求如下图: 就是A,B,C D ...
- 自定义一个可以被序列化的泛型Dictionary<TKey,TValue>集合
Dictionary是一个键值类型的集合.它有点像数组,但Dictionary的键可以是任何类型,内部使用Hash Table存储键和值.本篇自定义一个类型安全的泛型Dictionary<TKe ...
- C#编程(五十三)----------字典Dictionary<TKey,TValue>
字典 关键字:Dicitionary 说明: 必须包含命名空间System.Collection.Generic Dictionary里面的每一个元素都是一个键值对(由两个元组组成:键和值). 键必须 ...
随机推荐
- Hadoop2.6.0的事件分类与实现
前言 说实在的,在阅读Hadoop YARN的源码之前,我对于java枚举的使用相形见绌.YARN中实现的事件在可读性.可维护性.可扩展性方面的工作都值得借鉴. 概念 在具体分析源码之前,我们先看看Y ...
- C# 动态加载程序集dll (实现接口)
一.程序集(接口程序集):LyhInterface.Dll namespace LyhInterface { public interface ILyhInterface { void Run(); ...
- Vim自动补全神器–YouCompleteMe
YouCompleteMe的特别之处 基于语义补全 总所周知,Vim是一款文本编辑器.也就是说,其最基础的工作就是编辑文本,而不管该文本的内容是什么.在Vim被程序员所使用后,其慢慢的被肩负了与IDE ...
- css怎样让HTML中超出的内容显示为省略号
文字超出了需要隐藏并显示省略号这个在工作中很多时候都要用到,我想很多人都碰到过吧,这个有两种解决方法第一种.用程序开截取字符长度,这个其实也是可以的.第二种就是接下来分享的内容,用css样式来做,话也 ...
- PHP-递归扫描目录和删除目录
(1) 通过递归扫描目录并打印 // php递归扫描目录 function scanMyDir($path){ // 打开目录 $dh = opendir($path); echo '<ul&g ...
- 写在学AngularJS之前
近来从不同途径听说AngularJS和MEAN stack,感觉很有趣的样子,于是准备抽时间来学习一下.在这里记录学习过程中的笔记. 简单整理一下先: 1. 我的学习资料: a) 官网 b) wiki ...
- H3C qos 简单配置
qos 有三种服务模型 Best-Effort service(尽力而为服务模型) Integrated service(综合服务模型,简称Int-Serv) Differentiated servi ...
- html入门问题_2016-10-29
在mac机器上,用Safari打开html文件 1. 如果html里有中文,则在<head><meta http-equiv="Content-Type" con ...
- IOS对.Net返回的Base64string解析问题
1.c#生成Base64字符串的代码 string body = "{\"title\":\"上次CVR卡其\",\"url\": ...
- Linux内核分析之扒开系统调用的三层皮(下)
一.实验内容 1. 通过内核的方式使用系统调用 需要使用的命令 rm menu -rf //强制删除当前menugit clone http://github.com/mengning/menu.gi ...