godot/core/oa_hash_map.h

595 lines
16 KiB
C++

/*************************************************************************/
/* oa_hash_map.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef OA_HASH_MAP_H
#define OA_HASH_MAP_H
#include "hashfuncs.h"
#include "math_funcs.h"
#include "os/copymem.h"
#include "os/memory.h"
// uncomment this to disable initial local storage.
#define OA_HASH_MAP_INITIAL_LOCAL_STORAGE
/**
* This class implements a hash map datastructure that uses open addressing with
* local probing.
*
* It can give huge performance improvements over a chained HashMap because of
* the increased data locality.
*
* Because of that locality property it's important to not use "large" value
* types as the "TData" type. If TData values are too big it can cause more
* cache misses then chaining. If larger values are needed then storing those
* in a separate array and using pointers or indices to reference them is the
* better solution.
*
* This hash map also implements real-time incremental rehashing.
*
*/
template <class TKey, class TData,
uint16_t INITIAL_NUM_ELEMENTS = 64,
class Hasher = HashMapHasherDefault,
class Comparator = HashMapComparatorDefault<TKey> >
class OAHashMap {
private:
#ifdef OA_HASH_MAP_INITIAL_LOCAL_STORAGE
TData local_data[INITIAL_NUM_ELEMENTS];
TKey local_keys[INITIAL_NUM_ELEMENTS];
uint32_t local_hashes[INITIAL_NUM_ELEMENTS];
uint8_t local_flags[INITIAL_NUM_ELEMENTS / 4 + (INITIAL_NUM_ELEMENTS % 4 != 0 ? 1 : 0)];
#endif
struct {
TData *data;
TKey *keys;
uint32_t *hashes;
// This is actually an array of bits, 4 bit pairs per octet.
// | ba ba ba ba | ba ba ba ba | ....
//
// if a is set it means that there is an element present.
// if b is set it means that an element was deleted. This is needed for
// the local probing to work without relocating any succeeding and
// colliding entries.
uint8_t *flags;
uint32_t capacity;
} table, old_table;
bool is_rehashing;
uint32_t rehash_position;
uint32_t rehash_amount;
uint32_t elements;
/* Methods */
// returns true if the value already existed, false if it's a new entry
bool _raw_set_with_hash(uint32_t p_hash, const TKey &p_key, const TData &p_data) {
for (int i = 0; i < table.capacity; i++) {
int pos = (p_hash + i) % table.capacity;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = table.flags[flags_pos] & (1 << (2 * flags_pos_offset));
bool is_deleted_flag = table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1));
if (is_filled_flag) {
if (table.hashes[pos] == p_hash && Comparator::compare(table.keys[pos], p_key)) {
table.data[pos] = p_data;
return true;
}
continue;
}
table.keys[pos] = p_key;
table.data[pos] = p_data;
table.hashes[pos] = p_hash;
table.flags[flags_pos] |= (1 << (2 * flags_pos_offset));
table.flags[flags_pos] &= ~(1 << (2 * flags_pos_offset + 1));
return false;
}
return false;
}
public:
_FORCE_INLINE_ uint32_t get_capacity() const { return table.capacity; }
_FORCE_INLINE_ uint32_t get_num_elements() const { return elements; }
void set(const TKey &p_key, const TData &p_data) {
uint32_t hash = Hasher::hash(p_key);
// We don't progress the rehashing if the table just got resized
// to keep the cost of this function low.
if (is_rehashing) {
// rehash progress
for (int i = 0; i <= rehash_amount && rehash_position < old_table.capacity; rehash_position++) {
int flags_pos = rehash_position / 4;
int flags_pos_offset = rehash_position % 4;
bool is_filled_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
bool is_deleted_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1))) > 0;
if (is_filled_flag) {
_raw_set_with_hash(old_table.hashes[rehash_position], old_table.keys[rehash_position], old_table.data[rehash_position]);
old_table.keys[rehash_position].~TKey();
old_table.data[rehash_position].~TData();
memnew_placement(&old_table.keys[rehash_position], TKey);
memnew_placement(&old_table.data[rehash_position], TData);
old_table.flags[flags_pos] &= ~(1 << (2 * flags_pos_offset));
old_table.flags[flags_pos] |= (1 << (2 * flags_pos_offset + 1));
}
}
if (rehash_position >= old_table.capacity) {
// wohooo, we can get rid of the old table.
is_rehashing = false;
#ifdef OA_HASH_MAP_INITIAL_LOCAL_STORAGE
if (old_table.data == local_data) {
// Everything is local, so no cleanup :P
} else
#endif
{
memdelete_arr(old_table.data);
memdelete_arr(old_table.keys);
memdelete_arr(old_table.hashes);
memdelete_arr(old_table.flags);
}
}
}
// Table is almost full, resize and start rehashing process.
if (elements >= table.capacity * 0.7) {
old_table.capacity = table.capacity;
old_table.data = table.data;
old_table.flags = table.flags;
old_table.hashes = table.hashes;
old_table.keys = table.keys;
table.capacity = old_table.capacity * 2;
table.data = memnew_arr(TData, table.capacity);
table.flags = memnew_arr(uint8_t, table.capacity / 4 + (table.capacity % 4 != 0 ? 1 : 0));
table.hashes = memnew_arr(uint32_t, table.capacity);
table.keys = memnew_arr(TKey, table.capacity);
zeromem(table.flags, table.capacity / 4 + (table.capacity % 4 != 0 ? 1 : 0));
is_rehashing = true;
rehash_position = 0;
rehash_amount = (elements * 2) / (table.capacity * 0.7 - old_table.capacity);
}
if (!_raw_set_with_hash(hash, p_key, p_data))
elements++;
}
/**
* returns true if the value was found, false otherwise.
*
* if r_data is not NULL then the value will be written to the object
* it points to.
*/
bool lookup(const TKey &p_key, TData *r_data) {
uint32_t hash = Hasher::hash(p_key);
bool check_old_table = is_rehashing;
bool check_new_table = true;
// search for the key and return the value associated with it
//
// if we're rehashing we need to check both the old and the
// current table. If we find a value in the old table we still
// need to continue searching in the new table as it might have
// been added after
TData *value = NULL;
for (int i = 0; i < table.capacity; i++) {
if (!check_new_table && !check_old_table) {
break;
}
// if we're rehashing check the old table
if (check_old_table && i < old_table.capacity) {
int pos = (hash + i) % old_table.capacity;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
bool is_deleted_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1))) > 0;
if (is_filled_flag) {
// found our entry?
if (old_table.hashes[pos] == hash && Comparator::compare(old_table.keys[pos], p_key)) {
value = &old_table.data[pos];
check_old_table = false;
}
} else if (!is_deleted_flag) {
// we hit an empty field here, we don't
// need to further check this old table
// because we know it's not in here.
check_old_table = false;
}
}
if (check_new_table) {
int pos = (hash + i) % table.capacity;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
bool is_deleted_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1))) > 0;
if (is_filled_flag) {
// found our entry?
if (table.hashes[pos] == hash && Comparator::compare(table.keys[pos], p_key)) {
if (r_data != NULL)
*r_data = table.data[pos];
return true;
}
continue;
} else if (is_deleted_flag) {
continue;
} else if (value != NULL) {
// We found a value in the old table
if (r_data != NULL)
*r_data = *value;
return true;
} else {
check_new_table = false;
}
}
}
if (value != NULL) {
if (r_data != NULL)
*r_data = *value;
return true;
}
return false;
}
_FORCE_INLINE_ bool has(const TKey &p_key) {
return lookup(p_key, NULL);
}
void remove(const TKey &p_key) {
uint32_t hash = Hasher::hash(p_key);
bool check_old_table = is_rehashing;
bool check_new_table = true;
for (int i = 0; i < table.capacity; i++) {
if (!check_new_table && !check_old_table) {
return;
}
// if we're rehashing check the old table
if (check_old_table && i < old_table.capacity) {
int pos = (hash + i) % old_table.capacity;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
bool is_deleted_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1))) > 0;
if (is_filled_flag) {
// found our entry?
if (old_table.hashes[pos] == hash && Comparator::compare(old_table.keys[pos], p_key)) {
old_table.keys[pos].~TKey();
old_table.data[pos].~TData();
memnew_placement(&old_table.keys[pos], TKey);
memnew_placement(&old_table.data[pos], TData);
old_table.flags[flags_pos] &= ~(1 << (2 * flags_pos_offset));
old_table.flags[flags_pos] |= (1 << (2 * flags_pos_offset + 1));
elements--;
return;
}
} else if (!is_deleted_flag) {
// we hit an empty field here, we don't
// need to further check this old table
// because we know it's not in here.
check_old_table = false;
}
}
if (check_new_table) {
int pos = (hash + i) % table.capacity;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
bool is_deleted_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset + 1))) > 0;
if (is_filled_flag) {
// found our entry?
if (table.hashes[pos] == hash && Comparator::compare(table.keys[pos], p_key)) {
table.keys[pos].~TKey();
table.data[pos].~TData();
memnew_placement(&table.keys[pos], TKey);
memnew_placement(&table.data[pos], TData);
table.flags[flags_pos] &= ~(1 << (2 * flags_pos_offset));
table.flags[flags_pos] |= (1 << (2 * flags_pos_offset + 1));
// don't return here, this value might still be in the old table
// if it was already relocated.
elements--;
return;
}
continue;
} else if (is_deleted_flag) {
continue;
} else {
check_new_table = false;
}
}
}
}
struct Iterator {
bool valid;
uint32_t hash;
const TKey *key;
const TData *data;
private:
friend class OAHashMap;
bool was_from_old_table;
};
Iterator iter() const {
Iterator it;
it.valid = false;
it.was_from_old_table = false;
bool check_old_table = is_rehashing;
for (int i = 0; i < table.capacity; i++) {
// if we're rehashing check the old table first
if (check_old_table && i < old_table.capacity) {
int pos = i;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
if (is_filled_flag) {
it.valid = true;
it.hash = old_table.hashes[pos];
it.data = &old_table.data[pos];
it.key = &old_table.keys[pos];
it.was_from_old_table = true;
return it;
}
}
{
int pos = i;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
if (is_filled_flag) {
it.valid = true;
it.hash = table.hashes[pos];
it.data = &table.data[pos];
it.key = &table.keys[pos];
return it;
}
}
}
return it;
}
Iterator next_iter(const Iterator &p_iter) const {
if (!p_iter.valid) {
return p_iter;
}
Iterator it;
it.valid = false;
it.was_from_old_table = false;
bool check_old_table = is_rehashing;
// we use this to skip the first check or not
bool was_from_old_table = p_iter.was_from_old_table;
int prev_index = (p_iter.data - (p_iter.was_from_old_table ? old_table.data : table.data));
if (!was_from_old_table) {
prev_index++;
}
for (int i = prev_index; i < table.capacity; i++) {
// if we're rehashing check the old table first
if (check_old_table && i < old_table.capacity && !was_from_old_table) {
int pos = i;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (old_table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
if (is_filled_flag) {
it.valid = true;
it.hash = old_table.hashes[pos];
it.data = &old_table.data[pos];
it.key = &old_table.keys[pos];
it.was_from_old_table = true;
return it;
}
}
was_from_old_table = false;
{
int pos = i;
int flags_pos = pos / 4;
int flags_pos_offset = pos % 4;
bool is_filled_flag = (table.flags[flags_pos] & (1 << (2 * flags_pos_offset))) > 0;
if (is_filled_flag) {
it.valid = true;
it.hash = table.hashes[pos];
it.data = &table.data[pos];
it.key = &table.keys[pos];
return it;
}
}
}
return it;
}
OAHashMap(uint32_t p_initial_capacity = INITIAL_NUM_ELEMENTS) {
#ifdef OA_HASH_MAP_INITIAL_LOCAL_STORAGE
if (p_initial_capacity <= INITIAL_NUM_ELEMENTS) {
table.data = local_data;
table.keys = local_keys;
table.hashes = local_hashes;
table.flags = local_flags;
zeromem(table.flags, INITIAL_NUM_ELEMENTS / 4 + (INITIAL_NUM_ELEMENTS % 4 != 0 ? 1 : 0));
table.capacity = INITIAL_NUM_ELEMENTS;
elements = 0;
} else
#endif
{
table.data = memnew_arr(TData, p_initial_capacity);
table.keys = memnew_arr(TKey, p_initial_capacity);
table.hashes = memnew_arr(uint32_t, p_initial_capacity);
table.flags = memnew_arr(uint8_t, p_initial_capacity / 4 + (p_initial_capacity % 4 != 0 ? 1 : 0));
zeromem(table.flags, p_initial_capacity / 4 + (p_initial_capacity % 4 != 0 ? 1 : 0));
table.capacity = p_initial_capacity;
elements = 0;
}
is_rehashing = false;
rehash_position = 0;
}
~OAHashMap() {
#ifdef OA_HASH_MAP_INITIAL_LOCAL_STORAGE
if (table.capacity <= INITIAL_NUM_ELEMENTS) {
return; // Everything is local, so no cleanup :P
}
#endif
if (is_rehashing) {
#ifdef OA_HASH_MAP_INITIAL_LOCAL_STORAGE
if (old_table.data == local_data) {
// Everything is local, so no cleanup :P
} else
#endif
{
memdelete_arr(old_table.data);
memdelete_arr(old_table.keys);
memdelete_arr(old_table.hashes);
memdelete_arr(old_table.flags);
}
}
memdelete_arr(table.data);
memdelete_arr(table.keys);
memdelete_arr(table.hashes);
memdelete_arr(table.flags);
}
};
#endif