這周定位一個與DenseMap有關的問題, 正好趁機過一下它的實現.
DenseMap(稠密映射)是LLVM自定義的關系類的容器, 我們可以像std::map一樣使用它, 但要注意兩者之間稍稍有些區別.
概述
類似於unorderd_map, DenseMap也是通過哈希實現, 區別在於DenseMap使用單一分配方式一次性分配所有bucket的內存(用來存儲key-value pair), 因此具有較好的局部性.
由於其預分配內存的原因, 擴增哈希表會導致重新分配內存與拷貝(否則無法維護連續內存的特性), 因此增刪元素會導致迭代器失效(這點與std::map不同). 另外預分配也導致額外內存開銷(實際插入元素個數少於分配個數).
最后對於特殊的Key類型需要特化對應類型的DenseMapInfo結構, 提供包括獲取空鍵/獲取tombstone/獲取哈希結果/比較Key值四個接口.
官方文檔里有對它的介紹.
實現
LLVM提供了兩類稠密映射, DenseMap與SmallDenseMap, 兩者區別在於使用場景不同導致的內存分配方式不同, 因此我們重點關注DenseMap(defined in include/llvm/ADT/DenseMap.h).
template <typename DerivedT, typename KeyT, typename ValueT, typename KeyInfoT, typename BucketT>
class DenseMapBase : public DebugEpochBase {
protected:
DenseMapBase() = default;
};
template <typename KeyT, typename ValueT,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = llvm::detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, KeyInfoT, BucketT>,
KeyT, ValueT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, BucketT>;
using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, BucketT>;
BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;
public:
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
~DenseMap() {
this->destroyAll();
deallocate_buffer(Buckets, sizeof(BucketT) * NumBuckets, alignof(BucketT));
}
void init(unsigned InitNumEntries) {
auto InitBuckets = BaseT::getMinBucketToReserveForEntries(InitNumEntries);
if (allocateBuckets(InitBuckets)) {
this->BaseT::initEmpty();
} else {
NumEntries = 0;
NumTombstones = 0;
}
}
bool allocateBuckets(unsigned Num) {
NumBuckets = Num;
if (NumBuckets == 0) {
Buckets = nullptr;
return false;
}
Buckets = static_cast<BucketT *>(allocate_buffer(sizeof(BucketT) * NumBuckets, alignof(BucketT)));
return true;
}
};
DenseMapBase是DenseMap與SmallDenseMap的基類, 其僅定義了通用的Map操作, 本身並不存儲數據, 繼承類實現了容器功能並向父類聲明友元以供父類訪問, 借此實現算法與容器實現的分離.
DenseMap包含四個成員, 其中指針Buckets指向一塊存放類型為BucketT的數組地址, NumBuckets即當前數組個數, NumEntries即已使用的條目個數, NumTombstones表示被廢棄且未重新映射的條目個數.
這里模板參數BucketT允許用戶自定義Bucket類型, 只要提供了對應獲取Key與Value的接口即可(i.e. 可以用PointerIntPair). 默認使用DenseMapPair, 它是對std::pair的封裝.
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {
using std::pair<KeyT, ValueT>::pair;
KeyT &getFirst() { return std::pair<KeyT, ValueT>::first; }
const KeyT &getFirst() const { return std::pair<KeyT, ValueT>::first; }
ValueT &getSecond() { return std::pair<KeyT, ValueT>::second; }
const ValueT &getSecond() const { return std::pair<KeyT, ValueT>::second; }
};
回到DenseMap的構造函數, 其調用DenseMap::init()接受一個(默認為0的)參數InitialReserve作為DenseMap起始元素個數並申請對應元素個數的空間, 申請成功再調用BaseT::initEmpty()將所有的Key初始化為空.
void DenseMapBase<>::initEmpty() {
setNumEntries(0);
setNumTombstones(0);
const KeyT EmptyKey = getEmptyKey();
for (BucketT *B = getBuckets(), *E = getBucketsEnd(); B != E; ++B)
::new (&B->getFirst()) KeyT(EmptyKey);
}
static const KeyT DenseMapBase<>::getEmptyKey() {
static_assert(std::is_base_of<DenseMapBase, DerivedT>::value, "Must pass the derived type to this template!");
return KeyInfoT::getEmptyKey();
}
注意initEmpty()里獲取空鍵方式是調用KeyInfoT::getEmptyKey(), 而KeyInfoT作為模板參數指定了哈希的方式, LLVM提供了默認的哈希方式是DenseMapInfo(defined in include/llvm/ADT/DenseMapInfo.h).
template<typename T> struct DenseMapInfo {
//static inline T getEmptyKey();
//static inline T getTombstoneKey();
//static unsigned getHashValue(const T &Val);
//static bool isEqual(const T &LHS, const T &RHS);
};
template<typename T> struct DenseMapInfo<T*> {
static constexpr uintptr_t Log2MaxAlign = 12;
static inline T* getEmptyKey() {
uintptr_t Val = static_cast<uintptr_t>(-1);
Val <<= Log2MaxAlign;
return reinterpret_cast<T*>(Val);
}
static inline T* getTombstoneKey() {
uintptr_t Val = static_cast<uintptr_t>(-2);
Val <<= Log2MaxAlign;
return reinterpret_cast<T*>(Val);
}
static unsigned getHashValue(const T *PtrVal) {
return (unsigned((uintptr_t)PtrVal) >> 4) ^
(unsigned((uintptr_t)PtrVal) >> 9);
}
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};
模板類DenseMapInfo需要實現四個靜態方法:
- getEmptyKey(): 獲取空鍵, 默認初始化時會將所有的元素設置為空鍵以表明該bucket可用.
- getTombstoneKey(): 獲取tombstone Key. 由於存在可能的哈希沖突, 一個哈希值對應多個Key的情況, 此時刪除中間的元素時不能將鍵值設為空鍵, 否則查找無法繼續, 因此使用特殊標記(tombstone).
- getHashValue(): 對輸入Key做哈希運算.
- isEqual(): 由於存在可能的哈希沖突, 需要一個方法判斷給定的兩個Key是否為同一Key.
注意DenseMapInfo.h中已經特化了包含指針與基礎類型在內的許多數據結構的DenseMapInfo, 上面列舉了指針的特化模板作為參考. 如果你的鍵值是自定義類型, 那么需要實現對應的DenseMapInfo.
查詢與增刪
DenseMap使用與std::map一致的接口, 但實現上稍有不同. 先來看下find().
template<typename LookupKeyT>
bool DenseMapBase<>::LookupBucketFor(const LookupKeyT &Val, const BucketT *&FoundBucket) const {
const BucketT *BucketsPtr = getBuckets();
const unsigned NumBuckets = getNumBuckets();
if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}
const BucketT *FoundTombstone = nullptr;
const KeyT EmptyKey = getEmptyKey();
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) && !KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) && !FoundTombstone)
FoundTombstone = ThisBucket;
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}
iterator DenseMapBase<>::find(const_arg_type_t<KeyT> Val) {
BucketT *TheBucket;
if (LookupBucketFor(Val, TheBucket))
return makeIterator(TheBucket,
shouldReverseIterate<KeyT>() ? getBuckets() : getBucketsEnd(),
*this, true);
return end();
}
LookupBucketFor()是DenseMap底層的查找接口實現, 其返回給定鍵值的bucket指針(通過FoundBucket), 如果該鍵值存在則返回true, 否則返回false.
當鍵值不存在時LookupBucketFor()會記錄下第一個找到的tombstone或空鍵(方便插入時選擇LRU的索引), 但是注意只有僅當找到空鍵時才會返回(意味着沒有更多的沖突元素), 如果整個表里已完全被tombstone包含會導致此處死循環(需要在插入時保證避免出現這類情況).
DenseMapBase額外提供了find_as()同樣提供查詢功能, 其與find()的區別在於前者支持模板參數而不使用鍵值的類型, 這在構造鍵值元素比較困難(但比較操作比較簡單)的場景下可以提供一個高效的查詢方式.
舉個例子: 一個Value中通常只支持一個PoisoningVH, 那對於以PoisoningVH為鍵值的DenseMap無法構造兩個相同的鍵值, 此時可以使用其指向的Value本身查詢. 注意在使用find_as()時DenseMapInfo需要額外實現對應模板類型的getHashValue()與isEqual()接口.
template<class LookupKeyT>
iterator DenseMapBase<>::find_as(const LookupKeyT &Val) {
BucketT *TheBucket;
if (LookupBucketFor(Val, TheBucket))
return makeIterator(TheBucket,
shouldReverseIterate<KeyT>() ? getBuckets() : getBucketsEnd(),
*this, true);
return end();
}
再來看下插入接口, 若該鍵值對不存在則就地構造該值, insert()返回true, 否則返回false且不更新該值. InsertIntoBucket()嘗試尋找一個空bucket, 然后將KeyT與ValueT移動賦值給bucket. 這里查找bucket的步驟如下:
- 如果當前元素個數超過容量的3/4則將容器擴大一倍, 然后重新從擴容后的容器中查找一個bucket.
- 否則如果空鍵值的元素個數(即不包含使用中的元素與tombstone元素)小於總容量的1/8則將tombstone元素恢復為空鍵(防止上文查找中死循環).
- 自增使用計數, 如果插入位置是tombstone還要自減tombstone的使用計數.
template <typename LookupKeyT>
BucketT *DenseMapBase<>::InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup, BucketT *TheBucket) {
incrementEpoch();
unsigned NewNumEntries = getNumEntries() + 1;
unsigned NumBuckets = getNumBuckets();
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
this->grow(NumBuckets * 2);
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <= NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
assert(TheBucket);
incrementNumEntries();
const KeyT EmptyKey = getEmptyKey();
if (!KeyInfoT::isEqual(TheBucket->getFirst(), EmptyKey))
decrementNumTombstones();
return TheBucket;
}
template <typename KeyArg, typename... ValueArgs>
BucketT *DenseMapBase<>::InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key, ValueArgs &&... Values) {
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
TheBucket->getFirst() = std::forward<KeyArg>(Key);
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}
template <typename... Ts>
std::pair<iterator, bool> DenseMapBase<>::try_emplace(KeyT &&Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(makeIterator(TheBucket,
shouldReverseIterate<KeyT>() ? getBuckets() : getBucketsEnd(),
*this, true),
false);
TheBucket = InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
return std::make_pair(makeIterator(TheBucket,
shouldReverseIterate<KeyT>() ? getBuckets() : getBucketsEnd(),
*this, true),
true);
}
std::pair<iterator, bool> DenseMapBase<>::insert(const std::pair<KeyT, ValueT> &KV) {
return try_emplace(KV.first, KV.second);
}
在空余元素個數不足時DenseMap會調用繼承類的grow()接口為容器擴容. 不同與std::map, DenseMap采用重新分配內存並拷貝舊元素的方式保持數據的連續性. 注意:
- grow()要求容器最少個數保持64個.
- 拷貝舊元素時僅僅對使用中的鍵值元素做拷貝, 因此即使grow()不增長實際大小, 也會釋放tombstone增加可用空間.
void DenseMapBase<>::moveFromOldBuckets(BucketT *OldBucketsBegin, BucketT *OldBucketsEnd) {
initEmpty();
const KeyT EmptyKey = getEmptyKey();
const KeyT TombstoneKey = getTombstoneKey();
for (BucketT *B = OldBucketsBegin, *E = OldBucketsEnd; B != E; ++B) {
if (!KeyInfoT::isEqual(B->getFirst(), EmptyKey) &&
!KeyInfoT::isEqual(B->getFirst(), TombstoneKey)) {
BucketT *DestBucket;
bool FoundVal = LookupBucketFor(B->getFirst(), DestBucket);
assert(!FoundVal && "Key already in new map?");
DestBucket->getFirst() = std::move(B->getFirst());
::new (&DestBucket->getSecond()) ValueT(std::move(B->getSecond()));
incrementNumEntries();
B->getSecond().~ValueT();
}
B->getFirst().~KeyT();
}
}
void DenseMap::grow(unsigned AtLeast) {
unsigned OldNumBuckets = NumBuckets;
BucketT *OldBuckets = Buckets;
allocateBuckets(std::max<unsigned>(64, static_cast<unsigned>(NextPowerOf2(AtLeast-1))));
assert(Buckets);
if (!OldBuckets) {
this->BaseT::initEmpty();
return;
}
this->moveFromOldBuckets(OldBuckets, OldBuckets+OldNumBuckets);
deallocate_buffer(OldBuckets, sizeof(BucketT) * OldNumBuckets, alignof(BucketT));
}
迭代器
與其它LLVM自定義容器一樣, DenseMap也不支持標准的迭代器, 因此需要實現自定義的迭代器.
DenseMapIterator(defined in include/llvm/ADT/DenseMap.h)包含兩個成員, Ptr指向當前bucket, End指向容器結束地址.
迭代器通過調用AdvancePastEmptyBuckets()/RetreatPastEmptyBuckets()遍歷所有元素.
template <typename KeyT, typename ValueT, typename KeyInfoT, typename Bucket, bool IsConst>
class DenseMapIterator : DebugEpochBase::HandleBase {
friend class DenseMapIterator<KeyT, ValueT, KeyInfoT, Bucket, true>;
friend class DenseMapIterator<KeyT, ValueT, KeyInfoT, Bucket, false>;
public:
using value_type = typename std::conditional<IsConst, const Bucket, Bucket>::type;
using pointer = value_type *;
private:
pointer Ptr = nullptr;
pointer End = nullptr;
public:
DenseMapIterator(pointer Pos, pointer E, const DebugEpochBase &Epoch, bool NoAdvance = false)
: DebugEpochBase::HandleBase(&Epoch), Ptr(Pos), End(E) {
assert(isHandleInSync() && "invalid construction!");
if (NoAdvance) return;
if (shouldReverseIterate<KeyT>()) {
RetreatPastEmptyBuckets();
return;
}
AdvancePastEmptyBuckets();
}
private:
void AdvancePastEmptyBuckets() {
assert(Ptr <= End);
const KeyT Empty = KeyInfoT::getEmptyKey();
const KeyT Tombstone = KeyInfoT::getTombstoneKey();
while (Ptr != End && (KeyInfoT::isEqual(Ptr->getFirst(), Empty) ||
KeyInfoT::isEqual(Ptr->getFirst(), Tombstone)))
++Ptr;
}
void RetreatPastEmptyBuckets() {
assert(Ptr >= End);
const KeyT Empty = KeyInfoT::getEmptyKey();
const KeyT Tombstone = KeyInfoT::getTombstoneKey();
while (Ptr != End && (KeyInfoT::isEqual(Ptr[-1].getFirst(), Empty) ||
KeyInfoT::isEqual(Ptr[-1].getFirst(), Tombstone)))
--Ptr;
}
};
DenseMapIterator向DenseMapBase聲明了友元, 因此DenseMapBase可以通過makeIterator()構造迭代器.
iterator DenseMapBase<>::makeIterator(BucketT *P, BucketT *E,
DebugEpochBase &Epoch, bool NoAdvance=false) {
if (shouldReverseIterate<KeyT>()) {
BucketT *B = P == getBucketsEnd() ? getBuckets() : P + 1;
return iterator(B, E, Epoch, NoAdvance);
}
return iterator(P, E, Epoch, NoAdvance);
}
inline DenseMapBase<>::iterator begin() {
if (empty())
return end();
if (shouldReverseIterate<KeyT>())
return makeIterator(getBucketsEnd() - 1, getBuckets(), *this);
return makeIterator(getBuckets(), getBucketsEnd(), *this);
}
空間優化的DenseMap
注意到DenseMap最小要求分配64個元素, 如果map中存儲元素較少造成很大的浪費, 所以LLVM又定義了一個針對少量元素的SmallDenseMap.
SmallDenseMap與DenseMap的唯一區別是前者假定元素個數通常小於一個給定值, 因此默認初始化時會靜態初始化一個bucket數組. 在分配元素超過了限制后會退化為DenseMap.
template <typename KeyT, typename ValueT, unsigned InlineBuckets = 4,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = llvm::detail::DenseMapPair<KeyT, ValueT>>
class SmallDenseMap : public DenseMapBase<
SmallDenseMap<KeyT, ValueT, InlineBuckets, KeyInfoT, BucketT>, KeyT, ValueT, KeyInfoT, BucketT> {
friend class DenseMapBase<SmallDenseMap, KeyT, ValueT, KeyInfoT, BucketT>;
using BaseT = DenseMapBase<SmallDenseMap, KeyT, ValueT, KeyInfoT, BucketT>;
unsigned Small : 1;
unsigned NumEntries : 31;
unsigned NumTombstones;
struct LargeRep {
BucketT *Buckets;
unsigned NumBuckets;
};
AlignedCharArrayUnion<BucketT[InlineBuckets], LargeRep> storage;
};
SmallDenseMap與DenseMap基本類似, 區別在於:
- SmallDenseMap定義一個union(AlignedCharArrayUnion), 保存了InlineBuckets個BucketT元素的數組, 或是一個LargeRep結構. 前者是靜態構造的bucket數組, 后者等同於DenseMap中動態分配的bucket數組.
- SmallDenseMap定義一個Small標記指示如何理解storage的類型, 為true代表此時使用靜態數組, 否則為動態分配數組.
小結
- DenseMap通過預分配方式申請內存, 在插入元素時在已分配的地址上構造元素. 預分配的方式是申請一塊連續內存, 當需要擴容時會重新申請內存並發生內容拷貝.
- 增刪元素會導致DenseMap擴容/縮減, 進而引起迭代器失效. 另外擴容時會刪除舊緩存中的元素(析構Key與Value), 因此需要注意Key與Value成員的所有權問題.
- 使用自定義數據類型做Key時需要注意是否實現了對應的DenseMapInfo類, 如果不想關心這些瑣事, 保險的做法是指針用對象指針做Key.
- SmallDenseMap與DenseMap的主要區別是默認預分配內存方式不同, 前者假定容器通常不會插入超過給定個數的元素, 因此使用靜態數組. 當超過給定大小的元素后會退化為DenseMap一樣的實現, 因此在元素個數可估算時通常使用SmallDenseMap更為高效.