blob: 195c6a55e052b6faaa665831cd1c878bb143caa5 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#ifndef VM_HASH_TABLE_H_
#define VM_HASH_TABLE_H_
// Temporarily used when sorting the indices in EnumIndexHashTable.
// TODO(koda): Remove these dependencies before using in production.
#include <map>
#include <vector>
#include "platform/assert.h"
#include "vm/object.h"
namespace dart {
// OVERVIEW:
//
// Hash maps and hash sets all use RawArray as backing storage. At the lowest
// level is a generic open-addressing table that supports deletion.
// - HashTable
// The next layer provides ordering and iteration functionality:
// - UnorderedHashTable
// - EnumIndexHashTable
// - LinkedListHashTable (TODO(koda): Implement.)
// The utility class HashTables handles growth and conversion (e.g., converting
// a compact EnumIndexHashTable to an iteration-efficient LinkedListHashTable).
// The next layer fixes the payload size and provides a natural interface:
// - HashMap
// - HashSet
// Combining either of these with an iteration strategy, we get the templates
// intended for use outside this file:
// - UnorderedHashMap
// - EnumIndexHashMap
// - LinkedListHashMap
// - UnorderedHashSet
// - EnumIndexHashSet
// - LinkedListHashSet
// Each of these can be finally specialized with KeyTraits to support any set of
// lookup key types (e.g., look up a char* in a set of String objects), and
// any equality and hash code computation.
//
// The classes all wrap an Array handle, and metods like HashSet::Insert can
// trigger growth into a new RawArray, updating the handle. Debug mode asserts
// that 'Release' was called once to access the final array before destruction.
// NOTE: The handle returned by 'Release' is cleared by ~HashTable.
//
// Example use:
// typedef UnorderedHashMap<FooTraits> FooMap;
// ...
// FooMap cache(get_foo_cache());
// cache.UpdateOrInsert(name0, obj0);
// cache.UpdateOrInsert(name1, obj1);
// ...
// set_foo_cache(cache.Release());
//
// If you *know* that no mutating operations were called, you can optimize:
// ...
// obj ^= cache.GetOrNull(name);
// ASSERT(cache.Release().raw() == get_foo_cache());
//
// TODO(koda): When exposing these to Dart code, document and assert that
// KeyTraits methods must not run Dart code (since the C++ code doesn't check
// for concurrent modification).
// Open-addressing hash table template using a RawArray as backing storage.
//
// The elements of the array are partitioned into entries:
// [ header | metadata | entry0 | entry1 | ... | entryN ]
// Each entry contains a key, followed by zero or more payload components,
// and has 3 possible states: unused, occupied, or deleted.
// The header tracks the number of entries in each state.
// Any object except Object::sentinel() and Object::transition_sentinel()
// may be stored as a key. Any object may be stored in a payload.
//
// Parameters
// KeyTraits: defines static methods
// bool IsMatch(const Key& key, const Object& obj) and
// uword Hash(const Key& key) for any number of desired lookup key types.
// kPayloadSize: number of components of the payload in each entry.
// kMetaDataSize: number of elements reserved (e.g., for iteration order data).
template<typename KeyTraits, intptr_t kPayloadSize, intptr_t kMetaDataSize>
class HashTable : public ValueObject {
public:
typedef KeyTraits Traits;
// Uses 'isolate' for handle allocation. 'Release' must be called at the end
// to obtain the final table after potential growth/shrinkage.
HashTable(Isolate* isolate, RawArray* data)
: isolate_(isolate),
key_handle_(Object::Handle(isolate_)),
smi_handle_(Smi::Handle(isolate_)),
data_(&Array::Handle(isolate_, data)),
released_data_(NULL) {}
// Like above, except uses current isolate.
explicit HashTable(RawArray* data)
: isolate_(Isolate::Current()),
key_handle_(Object::Handle(isolate_)),
smi_handle_(Smi::Handle(isolate_)),
data_(&Array::Handle(isolate_, data)),
released_data_(NULL) {}
// Returns the final table. The handle is cleared when this HashTable is
// destroyed.
Array& Release() {
ASSERT(data_ != NULL);
ASSERT(released_data_ == NULL);
// Ensure that no methods are called after 'Release'.
released_data_ = data_;
data_ = NULL;
return *released_data_;
}
~HashTable() {
// In DEBUG mode, calling 'Release' is mandatory.
ASSERT(data_ == NULL);
if (released_data_ != NULL) {
*released_data_ = Array::null();
}
}
// Returns a backing storage size such that 'num_occupied' distinct keys can
// be inserted into the table.
static intptr_t ArrayLengthForNumOccupied(intptr_t num_occupied) {
// The current invariant requires at least one unoccupied entry.
// TODO(koda): Adjust if moving to quadratic probing.
intptr_t num_entries = num_occupied + 1;
return kFirstKeyIndex + (kEntrySize * num_entries);
}
// Initializes an empty table.
void Initialize() const {
ASSERT(data_->Length() >= ArrayLengthForNumOccupied(0));
smi_handle_ = Smi::New(0);
data_->SetAt(kOccupiedEntriesIndex, smi_handle_);
data_->SetAt(kDeletedEntriesIndex, smi_handle_);
for (intptr_t i = kHeaderSize; i < data_->Length(); ++i) {
data_->SetAt(i, Object::sentinel());
}
}
// Returns whether 'key' matches any key in the table.
template<typename Key>
bool ContainsKey(const Key& key) const {
return FindKey(key) != -1;
}
// Returns the entry that matches 'key', or -1 if none exists.
template<typename Key>
intptr_t FindKey(const Key& key) const {
ASSERT(NumOccupied() < NumEntries());
// TODO(koda): Add salt.
intptr_t probe = static_cast<uword>(KeyTraits::Hash(key)) % NumEntries();
// TODO(koda): Consider quadratic probing.
for (; ; probe = (probe + 1) % NumEntries()) {
if (IsUnused(probe)) {
return -1;
} else if (IsDeleted(probe)) {
continue;
} else {
key_handle_ = GetKey(probe);
if (KeyTraits::IsMatch(key, key_handle_)) {
return probe;
}
}
}
UNREACHABLE();
return -1;
}
// Sets *entry to either:
// - an occupied entry matching 'key', and returns true, or
// - an unused/deleted entry where a matching key may be inserted,
// and returns false.
template<typename Key>
bool FindKeyOrDeletedOrUnused(const Key& key, intptr_t* entry) const {
ASSERT(entry != NULL);
ASSERT(NumOccupied() < NumEntries());
intptr_t probe = static_cast<uword>(KeyTraits::Hash(key)) % NumEntries();
intptr_t deleted = -1;
// TODO(koda): Consider quadratic probing.
for (; ; probe = (probe + 1) % NumEntries()) {
if (IsUnused(probe)) {
*entry = (deleted != -1) ? deleted : probe;
return false;
} else if (IsDeleted(probe)) {
if (deleted == -1) {
deleted = probe;
}
} else {
key_handle_ = GetKey(probe);
if (KeyTraits::IsMatch(key, key_handle_)) {
*entry = probe;
return true;
}
}
}
UNREACHABLE();
return false;
}
// Sets the key of a previously unoccupied entry. This must not be the last
// unoccupied entry.
void InsertKey(intptr_t entry, const Object& key) const {
ASSERT(!IsOccupied(entry));
AdjustSmiValueAt(kOccupiedEntriesIndex, 1);
if (IsDeleted(entry)) {
AdjustSmiValueAt(kDeletedEntriesIndex, -1);
} else {
ASSERT(IsUnused(entry));
}
InternalSetKey(entry, key);
ASSERT(IsOccupied(entry));
ASSERT(NumOccupied() < NumEntries());
}
bool IsUnused(intptr_t entry) const {
return InternalGetKey(entry) == Object::sentinel().raw();
}
bool IsOccupied(intptr_t entry) const {
return !IsUnused(entry) && !IsDeleted(entry);
}
bool IsDeleted(intptr_t entry) const {
return InternalGetKey(entry) == Object::transition_sentinel().raw();
}
RawObject* GetKey(intptr_t entry) const {
ASSERT(IsOccupied(entry));
return InternalGetKey(entry);
}
RawObject* GetPayload(intptr_t entry, intptr_t component) const {
ASSERT(IsOccupied(entry));
return data_->At(PayloadIndex(entry, component));
}
void UpdatePayload(intptr_t entry,
intptr_t component,
const Object& value) const {
ASSERT(IsOccupied(entry));
ASSERT(0 <= component && component < kPayloadSize);
data_->SetAt(PayloadIndex(entry, component), value);
}
// Deletes both the key and payload of the specified entry.
void DeleteEntry(intptr_t entry) const {
ASSERT(IsOccupied(entry));
for (intptr_t i = 0; i < kPayloadSize; ++i) {
UpdatePayload(entry, i, Object::transition_sentinel());
}
InternalSetKey(entry, Object::transition_sentinel());
AdjustSmiValueAt(kOccupiedEntriesIndex, -1);
AdjustSmiValueAt(kDeletedEntriesIndex, 1);
}
intptr_t NumEntries() const {
return (data_->Length() - kFirstKeyIndex) / kEntrySize;
}
intptr_t NumUnused() const {
return NumEntries() - NumOccupied() - NumDeleted();
}
intptr_t NumOccupied() const {
return GetSmiValueAt(kOccupiedEntriesIndex);
}
intptr_t NumDeleted() const {
return GetSmiValueAt(kDeletedEntriesIndex);
}
Object& KeyHandle() const {
return key_handle_;
}
Smi& SmiHandle() const {
return smi_handle_;
}
protected:
static const intptr_t kOccupiedEntriesIndex = 0;
static const intptr_t kDeletedEntriesIndex = 1;
static const intptr_t kHeaderSize = kDeletedEntriesIndex + 1;
static const intptr_t kMetaDataIndex = kHeaderSize;
static const intptr_t kFirstKeyIndex = kHeaderSize + kMetaDataSize;
static const intptr_t kEntrySize = 1 + kPayloadSize;
intptr_t KeyIndex(intptr_t entry) const {
ASSERT(0 <= entry && entry < NumEntries());
return kFirstKeyIndex + (kEntrySize * entry);
}
intptr_t PayloadIndex(intptr_t entry, intptr_t component) const {
ASSERT(0 <= component && component < kPayloadSize);
return KeyIndex(entry) + 1 + component;
}
RawObject* InternalGetKey(intptr_t entry) const {
return data_->At(KeyIndex(entry));
}
void InternalSetKey(intptr_t entry, const Object& key) const {
data_->SetAt(KeyIndex(entry), key);
}
intptr_t GetSmiValueAt(intptr_t index) const {
ASSERT(Object::Handle(isolate(), data_->At(index)).IsSmi());
return Smi::Value(Smi::RawCast(data_->At(index)));
}
void SetSmiValueAt(intptr_t index, intptr_t value) const {
smi_handle_ = Smi::New(value);
data_->SetAt(index, smi_handle_);
}
void AdjustSmiValueAt(intptr_t index, intptr_t delta) const {
SetSmiValueAt(index, (GetSmiValueAt(index) + delta));
}
Isolate* isolate() const { return isolate_; }
Isolate* isolate_;
Object& key_handle_;
Smi& smi_handle_;
// Exactly one of these is non-NULL, depending on whether Release was called.
Array* data_;
Array* released_data_;
friend class HashTables;
};
// Table with unspecified iteration order. No payload overhead or metadata.
template<typename KeyTraits, intptr_t kUserPayloadSize>
class UnorderedHashTable : public HashTable<KeyTraits, kUserPayloadSize, 0> {
public:
typedef HashTable<KeyTraits, kUserPayloadSize, 0> BaseTable;
static const intptr_t kPayloadSize = kUserPayloadSize;
explicit UnorderedHashTable(RawArray* data) : BaseTable(data) {}
UnorderedHashTable(Isolate* isolate, RawArray* data)
: BaseTable(isolate, data) {}
// Note: Does not check for concurrent modification.
class Iterator {
public:
explicit Iterator(const UnorderedHashTable* table)
: table_(table), entry_(-1) {}
bool MoveNext() {
while (entry_ < (table_->NumEntries() - 1)) {
++entry_;
if (table_->IsOccupied(entry_)) {
return true;
}
}
return false;
}
intptr_t Current() {
return entry_;
}
private:
const UnorderedHashTable* table_;
intptr_t entry_;
};
// No extra book-keeping needed for Initialize, InsertKey, DeleteEntry.
};
// Table with insertion order, using one payload component for the enumeration
// index, and one metadata element for the next enumeration index.
template<typename KeyTraits, intptr_t kUserPayloadSize>
class EnumIndexHashTable
: public HashTable<KeyTraits, kUserPayloadSize + 1, 1> {
public:
typedef HashTable<KeyTraits, kUserPayloadSize + 1, 1> BaseTable;
static const intptr_t kPayloadSize = kUserPayloadSize;
static const intptr_t kNextEnumIndex = BaseTable::kMetaDataIndex;
EnumIndexHashTable(Isolate* isolate, RawArray* data)
: BaseTable(isolate, data) {}
explicit EnumIndexHashTable(RawArray* data) : BaseTable(data) {}
// Note: Does not check for concurrent modification.
class Iterator {
public:
explicit Iterator(const EnumIndexHashTable* table) : index_(-1) {
// TODO(koda): Use GrowableArray after adding stateful comparator support.
std::map<intptr_t, intptr_t> enum_to_entry;
for (intptr_t i = 0; i < table->NumEntries(); ++i) {
if (table->IsOccupied(i)) {
intptr_t enum_index =
table->GetSmiValueAt(table->PayloadIndex(i, kPayloadSize));
enum_to_entry[enum_index] = i;
}
}
for (std::map<intptr_t, intptr_t>::iterator it = enum_to_entry.begin();
it != enum_to_entry.end();
++it) {
entries_.push_back(it->second);
}
}
bool MoveNext() {
if (index_ < (static_cast<intptr_t>(entries_.size() - 1))) {
index_++;
return true;
}
return false;
}
intptr_t Current() {
return entries_[index_];
}
private:
intptr_t index_;
std::vector<intptr_t> entries_;
};
void Initialize() const {
BaseTable::Initialize();
BaseTable::SetSmiValueAt(kNextEnumIndex, 0);
}
void InsertKey(intptr_t entry, const Object& key) const {
BaseTable::InsertKey(entry, key);
BaseTable::SmiHandle() =
Smi::New(BaseTable::GetSmiValueAt(kNextEnumIndex));
BaseTable::UpdatePayload(entry, kPayloadSize, BaseTable::SmiHandle());
// TODO(koda): Handle possible Smi overflow from repeated insert/delete.
BaseTable::AdjustSmiValueAt(kNextEnumIndex, 1);
}
// No extra book-keeping needed for DeleteEntry.
};
class HashTables : public AllStatic {
public:
// Allocates and initializes a table.
template<typename Table>
static RawArray* New(intptr_t initial_capacity,
Heap::Space space = Heap::kNew) {
Table table(Array::New(
Table::ArrayLengthForNumOccupied(initial_capacity), space));
table.Initialize();
return table.Release().raw();
}
// Clears 'to' and inserts all elements from 'from', in iteration order.
// The tables must have the same user payload size.
template<typename From, typename To>
static void Copy(const From& from, const To& to) {
COMPILE_ASSERT(From::kPayloadSize == To::kPayloadSize);
to.Initialize();
ASSERT(from.NumOccupied() < to.NumEntries());
typename From::Iterator it(&from);
Object& obj = Object::Handle();
while (it.MoveNext()) {
intptr_t from_entry = it.Current();
obj = from.GetKey(from_entry);
intptr_t to_entry = -1;
const Object& key = obj;
bool present = to.FindKeyOrDeletedOrUnused(key, &to_entry);
ASSERT(!present);
to.InsertKey(to_entry, obj);
for (intptr_t i = 0; i < From::kPayloadSize; ++i) {
obj = from.GetPayload(from_entry, i);
to.UpdatePayload(to_entry, i, obj);
}
}
}
template<typename Table>
static void EnsureLoadFactor(double low, double high, const Table& table) {
double current = (1 + table.NumOccupied() + table.NumDeleted()) /
static_cast<double>(table.NumEntries());
if (low <= current && current < high) {
return;
}
double target = (low + high) / 2.0;
intptr_t new_capacity = (1 + table.NumOccupied()) / target;
Table new_table(New<Table>(
new_capacity,
table.data_->IsOld() ? Heap::kOld : Heap::kNew));
Copy(table, new_table);
*table.data_ = new_table.Release().raw();
}
// Serializes a table by concatenating its entries as an array.
template<typename Table>
static RawArray* ToArray(const Table& table, bool include_payload) {
const intptr_t entry_size = include_payload ? (1 + Table::kPayloadSize) : 1;
Array& result = Array::Handle(Array::New(table.NumOccupied() * entry_size));
typename Table::Iterator it(&table);
Object& obj = Object::Handle();
intptr_t result_index = 0;
while (it.MoveNext()) {
intptr_t entry = it.Current();
obj = table.GetKey(entry);
result.SetAt(result_index++, obj);
if (include_payload) {
for (intptr_t i = 0; i < Table::kPayloadSize; ++i) {
obj = table.GetPayload(entry, i);
result.SetAt(result_index++, obj);
}
}
}
return result.raw();
}
};
template<typename BaseIterTable>
class HashMap : public BaseIterTable {
public:
explicit HashMap(RawArray* data) : BaseIterTable(data) {}
HashMap(Isolate* isolate, RawArray* data) : BaseIterTable(isolate, data) {}
template<typename Key>
RawObject* GetOrNull(const Key& key, bool* present = NULL) const {
intptr_t entry = BaseIterTable::FindKey(key);
if (present != NULL) {
*present = (entry != -1);
}
return (entry == -1) ? Object::null() : BaseIterTable::GetPayload(entry, 0);
}
bool UpdateOrInsert(const Object& key, const Object& value) const {
EnsureCapacity();
intptr_t entry = -1;
bool present = BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry);
if (!present) {
BaseIterTable::InsertKey(entry, key);
}
BaseIterTable::UpdatePayload(entry, 0, value);
return present;
}
// Update the value of an existing key. Note that 'key' need not be an Object.
template<typename Key>
void UpdateValue(const Key& key, const Object& value) const {
intptr_t entry = BaseIterTable::FindKey(key);
ASSERT(entry != -1);
BaseIterTable::UpdatePayload(entry, 0, value);
}
// If 'key' is not present, maps it to 'value_if_absent'. Returns the final
// value in the map.
RawObject* InsertOrGetValue(const Object& key,
const Object& value_if_absent) const {
EnsureCapacity();
intptr_t entry = -1;
if (!BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry)) {
BaseIterTable::InsertKey(entry, key);
BaseIterTable::UpdatePayload(entry, 0, value_if_absent);
return value_if_absent.raw();
} else {
return BaseIterTable::GetPayload(entry, 0);
}
}
// Like InsertOrGetValue, but calls NewKey to allocate a key object if needed.
template<typename Key>
RawObject* InsertNewOrGetValue(const Key& key,
const Object& value_if_absent) const {
EnsureCapacity();
intptr_t entry = -1;
if (!BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry)) {
BaseIterTable::KeyHandle() =
BaseIterTable::BaseTable::Traits::NewKey(key);
BaseIterTable::InsertKey(entry, BaseIterTable::KeyHandle());
BaseIterTable::UpdatePayload(entry, 0, value_if_absent);
return value_if_absent.raw();
} else {
return BaseIterTable::GetPayload(entry, 0);
}
}
template<typename Key>
bool Remove(const Key& key) const {
intptr_t entry = BaseIterTable::FindKey(key);
if (entry == -1) {
return false;
} else {
BaseIterTable::DeleteEntry(entry);
return true;
}
}
void Clear() const {
BaseIterTable::Initialize();
}
protected:
void EnsureCapacity() const {
static const double kMaxLoadFactor = 0.75;
// We currently never shrink.
HashTables::EnsureLoadFactor(0.0, kMaxLoadFactor, *this);
}
};
template<typename KeyTraits>
class UnorderedHashMap : public HashMap<UnorderedHashTable<KeyTraits, 1> > {
public:
typedef HashMap<UnorderedHashTable<KeyTraits, 1> > BaseMap;
explicit UnorderedHashMap(RawArray* data) : BaseMap(data) {}
UnorderedHashMap(Isolate* isolate, RawArray* data) : BaseMap(isolate, data) {}
};
template<typename KeyTraits>
class EnumIndexHashMap : public HashMap<EnumIndexHashTable<KeyTraits, 1> > {
public:
typedef HashMap<EnumIndexHashTable<KeyTraits, 1> > BaseMap;
explicit EnumIndexHashMap(RawArray* data) : BaseMap(data) {}
EnumIndexHashMap(Isolate* isolate, RawArray* data) : BaseMap(isolate, data) {}
};
template<typename BaseIterTable>
class HashSet : public BaseIterTable {
public:
explicit HashSet(RawArray* data) : BaseIterTable(data) {}
HashSet(Isolate* isolate, RawArray* data) : BaseIterTable(isolate, data) {}
bool Insert(const Object& key) {
EnsureCapacity();
intptr_t entry = -1;
bool present = BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry);
if (!present) {
BaseIterTable::InsertKey(entry, key);
}
return present;
}
// If 'key' is not present, insert and return it. Else, return the existing
// key in the set (useful for canonicalization).
RawObject* InsertOrGet(const Object& key) const {
EnsureCapacity();
intptr_t entry = -1;
if (!BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry)) {
BaseIterTable::InsertKey(entry, key);
return key.raw();
} else {
return BaseIterTable::GetPayload(entry, 0);
}
}
// Like InsertOrGet, but calls NewKey to allocate a key object if needed.
template<typename Key>
RawObject* InsertNewOrGet(const Key& key) const {
EnsureCapacity();
intptr_t entry = -1;
if (!BaseIterTable::FindKeyOrDeletedOrUnused(key, &entry)) {
BaseIterTable::KeyHandle() =
BaseIterTable::BaseTable::Traits::NewKey(key);
BaseIterTable::InsertKey(entry, BaseIterTable::KeyHandle());
return BaseIterTable::KeyHandle().raw();
} else {
return BaseIterTable::GetKey(entry);
}
}
template<typename Key>
RawObject* GetOrNull(const Key& key, bool* present = NULL) const {
intptr_t entry = BaseIterTable::FindKey(key);
if (present != NULL) {
*present = (entry != -1);
}
return (entry == -1) ? Object::null() : BaseIterTable::GetKey(entry);
}
template<typename Key>
bool Remove(const Key& key) const {
intptr_t entry = BaseIterTable::FindKey(key);
if (entry == -1) {
return false;
} else {
BaseIterTable::DeleteEntry(entry);
return true;
}
}
void Clear() const {
BaseIterTable::Initialize();
}
protected:
void EnsureCapacity() const {
static const double kMaxLoadFactor = 0.75;
// We currently never shrink.
HashTables::EnsureLoadFactor(0.0, kMaxLoadFactor, *this);
}
};
template<typename KeyTraits>
class UnorderedHashSet : public HashSet<UnorderedHashTable<KeyTraits, 0> > {
public:
typedef HashSet<UnorderedHashTable<KeyTraits, 0> > BaseSet;
explicit UnorderedHashSet(RawArray* data) : BaseSet(data) {}
UnorderedHashSet(Isolate* isolate, RawArray* data) : BaseSet(isolate, data) {}
};
template<typename KeyTraits>
class EnumIndexHashSet : public HashSet<EnumIndexHashTable<KeyTraits, 0> > {
public:
typedef HashSet<EnumIndexHashTable<KeyTraits, 0> > BaseSet;
explicit EnumIndexHashSet(RawArray* data) : BaseSet(data) {}
EnumIndexHashSet(Isolate* isolate, RawArray* data) : BaseSet(isolate, data) {}
};
} // namespace dart
#endif // VM_HASH_TABLE_H_