本文翻译、整理自:Exploring Swift Dictionary's Implementation算法
Swift中字典具备如下特色:swift
Hashable
协议)和Value有不少种方式能够用于存储这些Key-Value对,Swift中字典采用了使用线性探测的开放寻址法。数组
咱们知道,哈希表不可避免会出现的问题是哈希值冲突,也就是两个不一样的Key可能具备相同的哈希值。线性探测是指,若是出现第二个Key的哈希值和第一个Key的哈希值冲突,则会检查第一个Key对应位置的后一个位置是否可用,若是可用则把第二个Key对应的Value放在这里,不然就继续向后寻找。性能优化
一个容量为8的字典,它实际上只能存储7个Key-Value对,这是由于字典须要至少一个空位置做为插入和查找过程当中的中止标记。咱们把这个位置称为“洞”。ide
举个例子,假设Key1和Key2具备相同的哈希值,它们都存储在字典中。如今咱们查找Key3对应的值。Key3的哈希值和前二者相同,但它不存在于字典中。查找时,首先从Key1所在的位置开始比较,由于不匹配因此比较Key2所在的位置,并且从理论上来讲只用比较这两个位置便可。若是Key2的后面是一个洞,就表示查找到此为止,不然还得继续向后查找。函数
在实际内存中,它的布局看上去是这样的:布局
建立字典时会分配一段连续的内存,其大小很容易计算:性能
size = capacity * (sizeof(Bitmap) + sizeof(Keys) + sizeof(Values))
优化
从逻辑上来看,字典的组成结构以下:ui
其中每一列称为一个bucket,其中存储了三样东西:位图的值,Key和Value。bucket的概念其实已经有些相似于咱们实际使用字典时,Key-Value对的概念了。
bucket中位图的值用于表示这个bucket中的Key和Value是不是已初始化且有效的。若是不是,那么这个bucket就是一个洞。
介绍完以上基本概念后,咱们由底层向高层介绍字典的实现原理:
这个结构体是字典所使用内存的头部,它有三个成员变量:
这个类是ManagedBuffer<_HashedContainerStorageHeader, UInt8>
的子类。
这个类的做用是为字典分配须要使用的内存,而且返回指向位图、Key和Value数组的指针。好比:
internal var _values: UnsafeMutablePointer<Value> {
@warn_unused_result
get {
let start = UInt(Builtin.ptrtoint_Word(_keys._rawValue)) &+
UInt(_capacity) &* UInt(strideof(Key.self))
let alignment = UInt(alignof(Value))
let alignMask = alignment &- UInt(1)
return UnsafeMutablePointer<Value>(
bitPattern:(start &+ alignMask) & ~alignMask)
}
}
复制代码
因为位图、Key和Value数组所在的内存是连续分配的,因此Value数组的指针values_pointer
等于keys_pointer + capacity * keys_pointer
。
分配字典所用内存的函数和下面的知识关系不大,因此这里略去不写,有兴趣的读者能够在原文中查看。
在分配内存的过程当中,位图数组中全部的元素值都是0,这就表示全部的bucket都是洞。另外须要强调的一点是,到目前为止(分配字典所用内存)范型Key没必要实现Hashable
协议。
目前,字典的结构组成示意图以下:
这个结构体将_NativeDictionaryStorageImpl
结构体封装为本身的buffer
属性,它还提供了一些方法将实际上有三个连续数组组成的字典内存转换成逻辑上的bucket数组。并且,这个结构体将bucket数组中的第一个bucket和最后一个bucket在逻辑上连接起来,从而造成了一个bucket环,也就是说当你到达bucket数组的末尾而且调用next
方法时,你又会回到bucket数组的开头。
在进行插入或查找操做时,咱们须要算出这个Key对应哪一个bucket。因为Key实现了Hashable
,因此它必定实现了hashValue
方法并返回一个整数值。但这个哈希值可能比字典容量还大,因此咱们须要压缩这个哈希值,以确保它属于区间[0, capacity)
:
@warn_unused_result
internal func _bucket(k: Key) -> Int {
return _squeezeHashValue(k.hashValue, 0..<capacity)
}
复制代码
经过_next
和_prev
函数,咱们能够遍历整个bucket数组,这里虽然使用了溢出运算符,但实际上并不会发生溢出,我的猜想是为了性能优化:
internal var _bucketMask: Int {
return capacity &- 1
}
@warn_unused_result
internal func _next(bucket: Int) -> Int {
return (bucket &+ 1) & _bucketMask
}
@warn_unused_result
internal func _prev(bucket: Int) -> Int {
return (bucket &- 1) & _bucketMask
}
复制代码
字典容量capacity
必定能够表示为2的多少次方,所以_bucketMask
这个属性若是用二进制表示,则必定所有由1组成。举个例子体验一下,假设capacity = 8
:
在插入一个键值对时,咱们首先计算出Key对应哪一个bucket,而后调用下面的方法把Key和Value写入到bucket中,同时把位图的值设置为true:
@_transparent
internal func initializeKey(k: Key, value v: Value, at i: Int) {
_sanityCheck(!isInitializedEntry(i))
(keys + i).initialize(k)
(values + i).initialize(v)
initializedEntries[i] = true
_fixLifetime(self)
}
复制代码
另外一个须要重点介绍的函数是_find
:
_find
函数用于找到Key对应的bucket_buckey(key)
函数的配合@warn_unused_result
internal
func _find(key: Key, _ startBucket: Int) -> (pos: Index, found: Bool) {
var bucket = startBucket
while true {
let isHole = !isInitializedEntry(bucket)
if isHole {
return (Index(nativeStorage: self, offset: bucket), false)
}
if keyAt(bucket) == key {
return (Index(nativeStorage: self, offset: bucket), true)
}
bucket = _next(bucket)
}
}
复制代码
_squeezeHashValue
函数的返回值就是Key对应的bucket的下标,不过须要考虑不一样的Key哈希值冲突的状况。_find
函数会找到下一个可用的洞,以便插入数据。_squeezeHashValue
函数的本质是对Key的哈希值再次求得哈希值,而一个优秀的哈希函数是提升性能的关键。_squeezeHashValue
函数基本上符合要求,不过目前唯一的缺点是哈希变换的种子仍是一个占位常量,有兴趣的读者能够阅读完整的函数实现,其中的seed
就是一个值为0xff51afd7ed558ccd
的常量:
func _squeezeHashValue(hashValue: Int, _ resultRange: Range<UInt>) -> UInt {
let mixedHashValue = UInt(bitPattern: _mixInt(hashValue))
let resultCardinality: UInt = resultRange.endIndex - resultRange.startIndex
if _isPowerOf2(resultCardinality) {
return mixedHashValue & (resultCardinality - 1)
}
return resultRange.startIndex + (mixedHashValue % resultCardinality)
}
func _mixUInt64(value: UInt64) -> UInt64 {
// Similar to hash_4to8_bytes but using a seed instead of length.
let seed: UInt64 = _HashingDetail.getExecutionSeed()
let low: UInt64 = value & 0xffff_ffff
let high: UInt64 = value >> 32
return _HashingDetail.hash16Bytes(seed &+ (low << 3), high)
}
static func getExecutionSeed() -> UInt64 {
// FIXME: This needs to be a per-execution seed. This is just a placeholder
// implementation.
let seed: UInt64 = 0xff51afd7ed558ccd
return _HashingDetail.fixedSeedOverride == 0 ? seed : fixedSeedOverride
}
static func hash16Bytes(low: UInt64, _ high: UInt64) -> UInt64 {
// Murmur-inspired hashing.
let mul: UInt64 = 0x9ddfea08eb382d69
var a: UInt64 = (low ^ high) &* mul
a ^= (a >> 47)
var b: UInt64 = (high ^ a) &* mul
b ^= (b >> 47)
b = b &* mul
return b
}
复制代码
目前,字典的结构总结以下:
这个类被用于管理字典的引用计数,以支持写时复制(COW)特性。因为Dictionary
和DictionaryIndex
都会引用实际存储区域,因此引用计数为2。不过写时复制的惟一性检查不考虑由DictionaryIndex
致使的引用,因此若是字典经过引用这个类的实例对象来管理引用计数值,问题就很容易处理。
/// This class is an artifact of the COW implementation. This class only
/// exists to keep separate retain counts separate for:
/// - `Dictionary` and `NSDictionary`,
/// - `DictionaryIndex`.
///
/// This is important because the uniqueness check for COW only cares about
/// retain counts of the first kind.
/// 这个类用于区分如下两种引用:
/// - `Dictionary` and `NSDictionary`,
/// - `DictionaryIndex`.
/// 这是由于写时复制的惟一性检查只考虑第一种引用
复制代码
如今,字典的结构变得有些复杂,难以理解了:
这个枚举类型中有两个成员,它们各自具备本身的关联值,分别表示Swift原生的字典和Cocoa的字典:
case Native(_NativeDictionaryStorageOwner<Key, Value>)
case Cocoa(_CocoaDictionaryStorage)
复制代码
这个枚举类型的主要功能是:
internal mutating func nativeUpdateValue( value: Value, forKey key: Key ) -> Value? {
var (i, found) = native._find(key, native._bucket(key))
let minCapacity = found
? native.capacity
: NativeStorage.getMinCapacity(
native.count + 1,
native.maxLoadFactorInverse)
let (_, capacityChanged) = ensureUniqueNativeStorage(minCapacity)
if capacityChanged {
i = native._find(key, native._bucket(key)).pos
}
let oldValue: Value? = found ? native.valueAt(i.offset) : nil
if found {
native.setKey(key, value: value, at: i.offset)
} else {
native.initializeKey(key, value: value, at: i.offset)
native.count += 1
}
return oldValue
}
复制代码
/// - parameter idealBucket: The ideal bucket for the element being deleted.
/// - parameter offset: The offset of the element that will be deleted.
/// Requires an initialized entry at offset.
internal mutating func nativeDeleteImpl( nativeStorage: NativeStorage, idealBucket: Int, offset: Int ) {
_sanityCheck(
nativeStorage.isInitializedEntry(offset), "expected initialized entry")
// remove the element
nativeStorage.destroyEntryAt(offset)
nativeStorage.count -= 1
// If we've put a hole in a chain of contiguous elements, some
// element after the hole may belong where the new hole is.
var hole = offset
// Find the first bucket in the contiguous chain
var start = idealBucket
while nativeStorage.isInitializedEntry(nativeStorage._prev(start)) {
start = nativeStorage._prev(start)
}
// Find the last bucket in the contiguous chain
var lastInChain = hole
var b = nativeStorage._next(lastInChain)
while nativeStorage.isInitializedEntry(b) {
lastInChain = b
b = nativeStorage._next(b)
}
// Relocate out-of-place elements in the chain, repeating until
// none are found.
while hole != lastInChain {
// Walk backwards from the end of the chain looking for
// something out-of-place.
var b = lastInChain
while b != hole {
let idealBucket = nativeStorage._bucket(nativeStorage.keyAt(b))
// Does this element belong between start and hole? We need
// two separate tests depending on whether [start,hole] wraps
// around the end of the buffer
let c0 = idealBucket >= start
let c1 = idealBucket <= hole
if start <= hole ? (c0 && c1) : (c0 || c1) {
break // Found it
}
b = nativeStorage._prev(b)
}
if b == hole { // No out-of-place elements found; we're done adjusting
break
}
// Move the found element into the hole
nativeStorage.moveInitializeFrom(nativeStorage, at: b, toEntryAt: hole)
hole = b
}
}
复制代码
这段代码理解起来可能比较费力,我想举一个例子来讲明就比较简单了,假设一开始有8个bucket,bucket中的value就是bucket的下标,最后一个bucket是洞:
Bucket数组中元素下标: {0, 1, 2, 3, 4, 5, 6, 7(Hole)}
bucket中存储的Value: {0, 1, 2, 3, 4, 5, 6, null}
复制代码
接下来咱们删除第五个bucket,这会在原地留下一个洞:
Bucket数组中元素下标: {0, 1, 2, 3, 4(Hole), 5, 6, 7(Hole)}
bucket中存储的Value: {0, 1, 2, 3, , 5, 6 }
复制代码
为了补上这个洞,咱们把最后一个bucket中的内容移到这个洞里,如今第五个bucket就不是洞了:
Bucket数组中元素下标: {0, 1, 2, 3, 4, 5, 6(Hole), 7(Hole)}
bucket中存储的Value: {0, 1, 2, 3, 6, 5, , }
复制代码
Dictionary
结构体持有一个_VariantDictionaryStorage
类型的枚举,做为本身的成员属性,因此整个字典完整的组成结构以下图所示: