HashMap Using Cuckoo Hashing in C++11


#1

Cuckoo Hashing is nice because it has worst-case constant time lookups. It was invented in 2001 by Pagh and Rodler. You can read more about it here: https://en.wikipedia.org/wiki/Cuckoo_hashing.

This implementation is resizable, so the two tables expand if needed. It’s recommended you use a power of 2 for the size if you specify one in the constructor. Currently it uses a simple hashing policy by default, and your typical standard allocator for memory allocations. The Hash policy is very simple if you’d like to swap it out for your own custom policy: a hash function used to hash keys for the first table, and a hash2 function for the second table.

hashmap.hh

[code=cpp]#ifndef HASHMAP_HH
#define HASHMAP_HH 1

#include
#include
#include
#include
#include “hash_policy.hh”

template <
typename KeyType,
typename ValueType,
typename Predicate = std::equal_to,
template class Hash = SimpleHashPolicy,
typename Allocator = std::allocator<std::pair<KeyType, ValueType>>

class HashMap : public Hash {
Allocator allocator;
std::size_t tableSize, tableSize2, size;
std::vector<std::pair<KeyType, ValueType>*> entries, entries2;
Predicate pred;

const unsigned int getNextPowerOfTwo() const noexcept {
unsigned int pow = 2;
while (pow * 2 < tableSize) pow *= 2;
return pow;
}

public:
HashMap(const std::size_t size = 64) {
if (size <= 1) {
throw std::range_error(“Table size must be greater than one.”);
}

this->size = size;
tableSize = size / 2;
tableSize2 = size / 2;
allocator = Allocator();
entries = std::vector<std::pair<KeyType, ValueType>*>(tableSize);
entries2 = std::vector<std::pair<KeyType, ValueType>*>(tableSize2);
pred = Predicate();

}

~HashMap() {
for (auto& entry : entries) {
if (entry != nullptr) {
allocator.destroy(entry);
allocator.deallocate(entry, 1);
}
}

for (auto& entry : entries2) {
  if (entry != nullptr) {
    allocator.destroy(entry);
    allocator.deallocate(entry, 1);
  }
}

}

std::pair<KeyType, ValueType>* get(const KeyType k) const noexcept {
const unsigned int entryIndex = Hash::hash(k, tableSize);
const unsigned int entryIndex2 = Hash::hash2(k, tableSize2);
std::pair<KeyType, ValueType>* element = nullptr;
// The element can be in one of two places, get the correct KV-pair.
if (entries[entryIndex] != nullptr && pred(entries[entryIndex]->first, k)) {
element = entries[entryIndex];
} else if (entries2[entryIndex2] != nullptr && pred(entries2[entryIndex2]->first, k)) {
element = entries2[entryIndex2];
}

return element;

}

bool set(const KeyType k, ValueType v) noexcept {
unsigned int entryIndex = 0;
unsigned int nextPowerOfTwo = getNextPowerOfTwo();
unsigned int tries = 0;
bool retried = false;
std::pair<KeyType, ValueType>* element = allocator.allocate(1);
allocator.construct(element, k, v);
while (true) {
while (tries < nextPowerOfTwo) {
entryIndex = Hash::hash(element->first, tableSize);
if (entries[entryIndex] == nullptr) {
entries[entryIndex] = element;
return true;
} else {
// Kick-out any already occupying entry.
tries++;
std::pair<KeyType, ValueType>* tmp = entries[entryIndex];
entries[entryIndex] = element;
// Use the kicked-out key to hash.
entryIndex = Hash::hash2(tmp->first, tableSize2);
// Second table has an empty entry – put it there.
if (entries2[entryIndex] == nullptr) {
entries2[entryIndex] = tmp;
return true;
} else {
// Otherwise, kick-out the element there and find a new place for it
// in the first table.
element = entries2[entryIndex];
entries2[entryIndex] = tmp;
}
}

    tries++;
  }

  if (retried) {
    break;
  }

  size *= 2;
  tableSize = size / 2;
  tableSize2 = size / 2;
  entries.resize(tableSize, nullptr);
  entries2.resize(tableSize2, nullptr);
  retried = true;
  tries = 0;
  nextPowerOfTwo = getNextPowerOfTwo();
}

return false;

}

bool deleteElement(const KeyType k) noexcept {
const unsigned int entryIndex = Hash::hash(k, tableSize);
const unsigned int entryIndex2 = Hash::hash2(k, tableSize2);
// The element can be in one of two places, delete the correct KV-pair.
if (entries[entryIndex] != nullptr && pred(entries[entryIndex]->first, k)) {
allocator.destroy(entries[entryIndex]);
allocator.deallocate(entries[entryIndex], 1);
entries[entryIndex] = nullptr;
return true;
} else if (entries2[entryIndex2] != nullptr && pred(entries2[entryIndex2]->first, k)) {
allocator.destroy(entries2[entryIndex2]);
allocator.deallocate(entries2[entryIndex2], 1);
entries2[entryIndex2] = nullptr;
return true;
}

return false;

}

bool exists(const KeyType k) const noexcept {
const unsigned int entryIndex = Hash::hash(k, tableSize);
const unsigned int entryIndex2 = Hash::hash2(k, tableSize2);
// The element can be in one of two places, delete the correct KV-pair.
if (entries[entryIndex] != nullptr && pred(entries[entryIndex]->first, k)) {
return true;
} else if (entries2[entryIndex2] != nullptr && pred(entries2[entryIndex2]->first, k)) {
return true;
}

return false;

}
};

#endif
[/code]

hash_policy.hh

[code=cpp]#ifndef HASH_POLICY_HH
#define HASH_POLICY_HH 1

#include

template
struct SimpleHashPolicy {
static const unsigned int hash(const KeyType k, const unsigned int tableSize) {
return k % tableSize;
}

static const unsigned int hash2(const KeyType k, const unsigned int tableSize) {
return static_cast(k / tableSize) % tableSize;
}
};

#endif
[/code]

test.cc

[code=cpp]#include
#include “hashmap.hh”

int main() {
try {
HashMap<int, int> map;
map.set(0, 123);
map.set(1, 234);
map.set(2, 345);
std::cout << map.get(0)->second << " " << map.get(1)->second << " " << map.get(2)->second << std::endl;
} catch (const std::range_error& e) {
std::cerr << e.what() << std::endl;
}

return 0;
}[/code]


#2

C++ templates are the ugliest thing ever.


#3

I googled cuckoo hashing and that led me down a chain of videos and now I know about bloom filters, ty


#4

Seconded


#5

Everyone always comments to this effect when I post C++… :confused: :confused: :expressionless: ;D :o :rolleyes: :eek: :mad: :frowning: :frowning: :frowning:


#6

I don’t know about ‘ugly’ so much as ‘different’. If you want C++ that’s really hard to understand then check out some parts of the boost library.


#7

Boost’s MPL is especially ugly in my opinion since it’s literally template meta-programming but due to support of older compilers you even need to abuse the hell out of macros to get some things done.


#8

Did anyone else come here because of a massive legend of zelda nostalgia blast from the word “Cuckoo” <- Yea I know it’s not the same.


#9

wtf