本文翻譯、整理自: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
類型的枚舉,做爲本身的成員屬性,因此整個字典完整的組成結構以下圖所示: