HashTable是v8中哈希表的实现,HashTable继承Array。HashTable提供一些公共的逻辑,供后续子类使用。我们看一下他 大内存布局。
然后看一下类的定义。HashTable是个模板类,prefix_size是,element_size是哈希表中每个元素的大小。
template<int prefix_size, int element_size>
class HashTable: public FixedArray {
public:
// 哈希表已使用的元素个数
int NumberOfElements() {
return Smi::cast(get(kNumberOfElementsIndex))->value();
}
// 哈希表容量
int Capacity() {
return Smi::cast(get(kCapacityIndex))->value();
}
// 新增一个元素,修改已使用元素个数字段的值
void ElementAdded() { SetNumberOfElements(NumberOfElements() + 1); }
// 删除一个元素或n个元素时,修改已使用元素个数字段的值
void ElementRemoved() { SetNumberOfElements(NumberOfElements() - 1); }
void ElementsRemoved(int n) { SetNumberOfElements(NumberOfElements() - n); }
// 分配一个新的哈希表
static Object* Allocate(int at_least_space_for);
// 根据索引获取数组中对应的值
Object* KeyAt(int entry) { return get(EntryToIndex(entry)); }
// 是否是有效的键
bool IsKey(Object* k) {
return !k->IsNull() && !k->IsUndefined();
}
// 类型转换
static inline HashTable* cast(Object* obj);
// 管理哈希表key的类,由子类实现
class Key {
public:
// 判断key是否和Object中的相等相等
virtual bool IsMatch(Object* other) = 0;
// 哈希函数
typedef uint32_t (*HashFunction)(Object* obj);
// Returns the hash function used for this key.
virtual HashFunction GetHashFunction() = 0;
// 哈希值
virtual uint32_t Hash() = 0;
// 获取对应的值
virtual Object* GetObject() = 0;
virtual bool IsStringKey() = 0;
// Required.
virtual ~Key() {}
};
// 计算哈希冲突时,跳的步长
INLINE(static uint32_t GetProbeOffset(uint32_t n)) {
return (n + n * n) >> 1;
}
// 元素个数 索引
static const int kNumberOfElementsIndex = 0;
// 容量 索引
static const int kCapacityIndex = 1;
// prefix 索引
static const int kPrefixStartIndex = 2;
// 哈希表第一个元素的开始地址的索引
static const int kElementsStartIndex = kPrefixStartIndex + prefix_size;
// 元素大小
static const int kElementSize = element_size;
// 第一个元素的相对内存偏移地址
static const int kElementsStartOffset =
kHeaderSize + kElementsStartIndex * kPointerSize;
protected:
int FindEntry(Key* key);
uint32_t FindInsertionEntry(Object* key, uint32_t hash);
// 根据元素的索引算出在哈希表中的相对真正偏移
static inline int EntryToIndex(int entry) {
return (entry * kElementSize) + kElementsStartIndex;
}
// 设置哈希表中已使用元素的个数
void SetNumberOfElements(int nof) {
fast_set(this, kNumberOfElementsIndex, Smi::FromInt(nof));
}
// 设置哈希表容量
void SetCapacity(int capacity) {
ASSERT(capacity > 0);
fast_set(this, kCapacityIndex, Smi::FromInt(capacity));
}
// 哈希冲突时,下一个索引的位置,取余保证位置再哈希表有效元素的范围
static uint32_t GetProbe(uint32_t hash, uint32_t number, uint32_t size) {
ASSERT(IsPowerOf2(size));
return (hash + GetProbeOffset(number)) & (size - 1);
}
// 扩容
Object* EnsureCapacity(int n, Key* key);
};
接下来我们看一下在类外定义的内容。
1 Allocate
Allocate分配一个新的哈希表。
template<int prefix_size, int element_size>
Object* HashTable<prefix_size, element_size>::Allocate(int at_least_space_for) {
// 计算大于at_least_space_for的值,值是2的n次方中的最小值
int capacity = NextPowerOf2(at_least_space_for);
// 根据内存布局,不能小于4
if (capacity < 4) capacity = 4;
// 算出最后一个元素在哈希表中的相对偏移,即哈希表(数组)的大小,然后分配一个新的数组
Object* obj = Heap::AllocateHashTable(EntryToIndex(capacity));
if (!obj->IsFailure()) {
// 初始化属性
HashTable::cast(obj)->SetNumberOfElements(0);
HashTable::cast(obj)->SetCapacity(capacity);
}
return obj;
}
2 FindInsertionEntry
FindInsertionEntry通过key计算插入位置的索引,该索引是相对偏移位置。因为哈希表(数组)的内存布局中,前面几个元素是用于存储其他数据,所以从第n个元素起才是真正的元素。FindInsertionEntry返回的索引还需要转成真正的偏移。
template<int prefix_size, int element_size>
uint32_t HashTable<prefix_size, element_size>::FindInsertionEntry(
Object* key,
uint32_t hash) {
uint32_t capacity = Capacity();
// 通过key找到一个插入的位置
uint32_t entry = GetProbe(hash, 0, capacity);
// 该位置是否有值,有值说明发生冲突
Object* element = KeyAt(entry);
// 非空则说明冲突了,找下一个可用的索引,否则直接返回(如果都有值了,这里会死循环)
for (uint32_t i = 1; !(element->IsUndefined() || element->IsNull()); i++) {
entry = GetProbe(hash, i, capacity);
element = KeyAt(entry);
}
return entry;
}
3 EnsureCapacity
EnsureCapacity实现哈希表的扩容
template<int prefix_size, int element_size>
Object* HashTable<prefix_size, element_size>::EnsureCapacity(int n, Key* key) {
// 旧的容量
int capacity = Capacity();
// 已使用加上待添加的元素个数
int nof = NumberOfElements() + n;
// Make sure 20% is free
// n+n/4如果小于容量则说明剩余空间小于20%了。假设是等于,说明n/4==capacity/5
if (nof + (nof >> 2) <= capacity) return this;
// 分配一个新的数组
Object* obj = Allocate(nof * 2);
if (obj->IsFailure()) return obj;
HashTable* dict = HashTable::cast(obj);
WriteBarrierMode mode = dict->GetWriteBarrierMode();
// 把prefix先复制过去
for (int i = kPrefixStartIndex; i < kPrefixStartIndex + prefix_size; i++) {
dict->set(i, get(i), mode);
}
// 把数据从原来的哈希表复制到新的哈希表
uint32_t (*Hash)(Object* key) = key->GetHashFunction();
// 遍历之前的哈希表(数组)
for (int i = 0; i < capacity; i++) {
// 根据逻辑索引算出真正的索引
uint32_t from_index = EntryToIndex(i);
// 拿到索引的对应的元素
Object* key = get(from_index);
// 有值
if (IsKey(key)) {
// 重新哈希,并且找到在新哈希表的逻辑位置,再算出实际的位置
uint32_t insertion_index =
EntryToIndex(dict->FindInsertionEntry(key, Hash(key)));
// 写入新的哈希表,需要复制的大小由element_size决定,可能需要复制多个指针的值
for (int j = 0; j < element_size; j++) {
dict->set(insertion_index + j, get(from_index + j), mode);
}
}
}
// 设置已使用元素的个数
dict->SetNumberOfElements(NumberOfElements());
return dict;
}
2 FindEntry
FindEntry是通过key找到对应的位置。该位置是相对位置,存取的时候还需要计算出真正的位置。
template <int prefix_size, int element_size>
int HashTable<prefix_size, element_size>::FindEntry(Key* key) {
// 已使用的元素个数
uint32_t nof = NumberOfElements();
if (nof == 0) return -1; // Bail out if empty.
uint32_t capacity = Capacity();
uint32_t hash = key->Hash();
// 获取在哈希表中的逻辑索引,还需要转成内部真正的索引,因为哈希表的数组前几项用来存储其他数据了
uint32_t entry = GetProbe(hash, 0, capacity);
// 获取对应的键
Object* element = KeyAt(entry);
uint32_t passed_elements = 0;
// 有值
if (!element->IsNull()) {
// 匹配则直接返回
if (!element->IsUndefined() && key->IsMatch(element)) return entry;
// 不匹配则判断是不是所有元素都找过了,是则直接返回找不到
if (++passed_elements == nof) return -1;
}
// 继续找...
/*
因为哈希表解决冲突方式是开发地址法,所以继续找下一个位置的元素,
直到undefined,说明到底了,极端的情况下,遍历的每个元素都有值了,
但都不等于想要找的值,所以下面passed_elements那里也需要判断,
判断是不是遍历了所有值了,否则死循环
*/
for (uint32_t i = 1; !element->IsUndefined(); i++) {
entry = GetProbe(hash, i, capacity);
element = KeyAt(entry);
if (!element->IsNull()) {
if (!element->IsUndefined() && key->IsMatch(element)) return entry;
// todo
if (++passed_elements == nof) return -1;
}
}
return -1;
}