Browse Source

WIP probing hash map

Kajetan Johannes Hammerle 10 months ago
parent
commit
60102883a6

+ 286 - 0
data/ProbingHashMap.h

@@ -0,0 +1,286 @@
+#ifndef CORE_PROBING_HASHMAP_H
+#define CORE_PROBING_HASHMAP_H
+
+#include "data/List.h"
+#include "utils/ArrayString.h"
+#include "utils/HashCode.h"
+#include "utils/Logger.h"
+
+namespace Core {
+    template<typename K, typename V>
+    struct ProbingHashMap final {
+        class Node final {
+            friend ProbingHashMap;
+            friend List<Node>;
+            K key;
+
+        public:
+            V& value;
+
+            const K& getKey() const {
+                return key;
+            }
+
+            template<typename String>
+            check_return Error toString(String& s) const {
+                CORE_RETURN_ERROR(s.append(key));
+                CORE_RETURN_ERROR(s.append(" = "));
+                return s.append(value);
+            }
+
+        private:
+            Node(const K& key_, V& value_) : key(key_), value(value_) {
+            }
+        };
+
+    private:
+        /*template<typename N, typename I, typename R, R& (*A)(I&)>
+        class Iterator final {
+            N iterator;
+
+        public:
+            Iterator(N iterator_) : iterator(iterator_) {
+            }
+
+            Iterator& operator++() {
+                ++iterator;
+                return *this;
+            }
+
+            bool operator!=(const Iterator& other) const {
+                return iterator != other.iterator;
+            }
+
+            R& operator*() const {
+                return A(*iterator);
+            }
+        };
+
+        template<typename R>
+        static R& access(R& node) {
+            return node;
+        }
+
+        template<typename I, typename R>
+        static R& accessValue(I& node) {
+            return node.value;
+        }
+
+        static const K& accessKey(const Node& node) {
+            return node.getKey();
+        }
+
+        template<typename N, typename R>
+        using BaseEntryIterator = Iterator<N, R, R, access<R>>;
+        using EntryIterator = BaseEntryIterator<NodeIterator, Node>;
+        using ConstEntryIterator =
+            BaseEntryIterator<ConstNodeIterator, const Node>;
+
+        template<typename N, typename I, typename R>
+        using BaseValueIterator = Iterator<N, I, R, accessValue<I, R>>;
+        using ValueIterator = BaseValueIterator<NodeIterator, Node, V>;
+        using ConstValueIterator =
+            BaseValueIterator<ConstNodeIterator, const Node, const V>;
+
+        using ConstKeyIterator =
+            Iterator<ConstNodeIterator, const Node, const K, accessKey>;
+
+        template<typename M, typename I>
+        struct IteratorAdapter final {
+            M& map;
+
+            I begin() const {
+                return I(map.nodes.begin());
+            }
+
+            I end() const {
+                return I(map.nodes.end());
+            }
+        };
+
+        using EntryIteratorAdapter =
+            IteratorAdapter<ProbingHashMap, EntryIterator>;
+        using ConstEntryIteratorAdapter =
+            IteratorAdapter<const ProbingHashMap, ConstEntryIterator>;
+
+        using ValueIteratorAdapter =
+            IteratorAdapter<ProbingHashMap, ValueIterator>;
+        using ConstValueIteratorAdapter =
+            IteratorAdapter<const ProbingHashMap, ConstValueIterator>;
+
+        using ConstKeyIteratorAdapter =
+            IteratorAdapter<const ProbingHashMap, ConstKeyIterator>;*/
+
+    private:
+        List<K> keys;
+        List<V> values;
+        K emptyKey;
+        int entries;
+
+    public:
+        ProbingHashMap(const K& emptyKey_) : emptyKey(emptyKey_), entries(0) {
+        }
+
+        /*check_return Error copyFrom(const ProbingHashMap& other) {
+            ProbingHashMap copy;
+            for(const auto& en : other) {
+                CORE_RETURN_ERROR(copy.add(en.getKey(), en.value));
+            }
+            swap(copy.nodes, nodes);
+            swap(copy.nodePointers, nodePointers);
+            return Error::NONE;
+        }*/
+
+        check_return Error rehash(int minCapacity) {
+            minCapacity *= 2;
+            if(minCapacity <= keys.getLength()) {
+                return Error::NONE;
+            }
+            ProbingHashMap<K, V> map(emptyKey);
+            int l = 1 << Math::roundUpLog2(Core::Math::max(minCapacity, 8));
+            CORE_RETURN_ERROR(map.keys.resize(l, emptyKey));
+            CORE_RETURN_ERROR(map.values.resize(l));
+            for(int i = 0; i < keys.getLength(); i++) {
+                if(keys[i] != emptyKey) {
+                    CORE_RETURN_ERROR(map.add(keys[i], values[i]));
+                }
+            }
+            Core::swap(map.keys, keys);
+            Core::swap(map.values, values);
+            return Error::NONE;
+        }
+
+        /*template<typename... Args>
+        check_return Error tryEmplace(V*& v, const K& key, Args&&... args) {
+            CORE_RETURN_ERROR(rehash(nodes.getLength() + 1));
+            int h = hashIndex(key);
+            v = searchList(key, h);
+            if(v != nullptr) {
+                return Error::EXISTING_KEY;
+            }
+            NodePointer np = nullptr;
+            CORE_RETURN_ERROR(nodes.put(np, key, Core::forward<Args>(args)...));
+            Error e = Error::NONE;
+            if(checkError(e, nodePointers[h].add(np))) {
+                nodes.remove(np);
+                return e;
+            }
+            v = &(np->data.value);
+            return Error::NONE;
+        }*/
+
+        template<typename VA>
+        check_return Error put(V*& v, const K& key, VA&& value) {
+            CORE_RETURN_ERROR(rehash(entries + 1));
+            int index = searchList(key);
+            if(index < 0) {
+                return Error::CAPACITY_REACHED;
+            }
+            entries += keys[index] != key;
+            keys[index] = key;
+            values[index] = Core::forward<VA>(value);
+            v = &(values[index]);
+            return Error::NONE;
+        }
+
+        template<typename VA>
+        check_return Error add(const K& key, VA&& value) {
+            V* v = nullptr;
+            return put(v, key, Core::forward<VA>(value));
+        }
+
+        /*check_return Error remove(const K& key) {
+            NodePointerList& list = nodePointers[hashIndex(key)];
+            for(int i = 0; i < list.getLength(); i++) {
+                if(list[i]->data.key == key) {
+                    nodes.remove(list[i]);
+                    return list.removeBySwap(i);
+                }
+            }
+            return Error::NOT_FOUND;
+        }*/
+
+        const V* search(const K& key) const {
+            int i = searchList(key);
+            return i < 0 || keys[i] == emptyKey ? nullptr : &(values[i]);
+        }
+
+        V* search(const K& key) {
+            int i = searchList(key);
+            return i < 0 || keys[i] == emptyKey ? nullptr : &(values[i]);
+        }
+
+        bool contains(const K& key) const {
+            return search(key) != nullptr;
+        }
+
+        /*ProbingHashMap& clear() {
+            nodes.clear();
+            for(NodePointerList& n : nodePointers) {
+                n.clear();
+            }
+            return *this;
+        }*/
+
+        /*EntryIteratorAdapter entries() {
+            return {*this};
+        }
+
+        ConstEntryIteratorAdapter entries() const {
+            return {*this};
+        }*/
+
+        /*ConstKeyIteratorAdapter keys() const {
+            return keys.begin();
+        }
+
+        ValueIteratorAdapter values() {
+            return values.begin();
+        }
+
+        ConstValueIteratorAdapter values() const {
+            return values.begin();
+        }*/
+
+        /*EntryIterator begin() {
+            return EntryIterator(nodes.begin());
+        }
+
+        EntryIterator end() {
+            return EntryIterator(nodes.end());
+        }
+
+        ConstEntryIterator begin() const {
+            return ConstEntryIterator(nodes.begin());
+        }
+
+        ConstEntryIterator end() const {
+            return ConstEntryIterator(nodes.end());
+        }
+
+        template<typename String>
+        check_return Error toString(String& s) const {
+            return Core::toString(s, *this);
+        }*/
+
+    private:
+        int searchList(const K& key) const {
+            int baseHash = static_cast<int>(hashCode(key));
+            int end = keys.getLength() - 1;
+            for(int i = 0; i <= end; i++) {
+                int hash = (baseHash + i) & end;
+                if(keys[hash] == emptyKey || keys[hash] == key) {
+                    return hash;
+                }
+            }
+            return -1;
+        }
+
+        /*V* searchList(const K& key, int h) {
+            return const_cast<V*>(
+                static_cast<const ProbingHashMap*>(this)->searchList(key, h));
+        }*/
+    };
+}
+
+#endif

+ 33 - 21
meson.build

@@ -33,6 +33,7 @@ src_tests = [
     'tests/LinkedListTests.cpp',
     'tests/UniquePointerTests.cpp',
     'tests/HashMapTests.cpp',
+    'tests/ProbingHashMapTests.cpp',
     'tests/StackTests.cpp',
     'tests/RingBufferTests.cpp',
     'tests/ComponentsTests.cpp',
@@ -53,32 +54,37 @@ src_tests = [
     'tests/FileTests.cpp',
 ]
 
+src_performance = [
+    'performance/Main.cpp',
+    'test/Test.cpp',
+]
+
 compiler = meson.get_compiler('cpp')
 error_args = compiler.get_supported_arguments([
     '-Wdeprecated-enum-float-conversion', '-Wctad-maybe-unsupported', '-Werror',
     '-Wdeprecated-enum-enum-conversion', '-Winvalid-imported-macros', '-Wextra',
     '-Wzero-as-null-pointer-constant', '-Wframe-larger-than=8388608', '-Wundef',
-    '-Wsuggest-attribute=const', '-Wunused-const-variable=2', '-Wwrite-strings',
-    '-Wconditionally-supported', '-Wimplicit-fallthrough=5', '-pedantic-errors',
-    '-Wlarger-than=1073741824', '-Wtrivial-auto-var-init', '-Wdouble-promotion',
-    '-Wsuggest-final-methods', '-Wdisabled-optimization', '-Wformat-signedness',
-    '-Wmissing-declarations', '-Wdeprecated-copy-dtor', '-Wduplicated-branches',
-    '-Wmultiple-inheritance', '-Wstrict-null-sentinel', '-Wformat-truncation=2',
-    '-Wanalyzer-too-complex', '-Wmissing-include-dirs', '-Wstack-usage=8388608',
-    '-Wstringop-overflow=4', '-Wsuggest-final-types', '-Wconversion', '-Wsynth',
-    '-Wvirtual-inheritance', '-Woverlength-strings', '-Wlogical-op', '-Wshadow',
-    '-Woverloaded-virtual', '-Winfinite-recursion', '-Wswitch-enum', '-Walloca',
-    '-Wattribute-alias=2', '-Wformat-overflow=2', '-Wfloat-equal', '-Wformat=2',
-    '-Wctor-dtor-privacy', '-Wstrict-overflow=5', '-Winvalid-pch', '-Wvolatile',
-    '-Wcast-align=strict', '-Wshift-overflow=2', '-Wcatch-value=3', '-pedantic',
-    '-Wsuggest-override', '-Wuse-after-free=3', '-Wunused-macros', '-Wnoexcept',
-    '-Wnon-virtual-dtor', '-Wnull-dereference', '-Wtrampolines', '-Walloc-zero',
-    '-Warith-conversion', '-Wsign-conversion', '-Wbidi-chars=any', '-Wregister',
-    '-Wcomma-subscript', '-Warray-parameter', '-Wold-style-cast', '-Winit-self',
-    '-Wplacement-new=2', '-Wstack-protector', '-Wmissing-braces', '-Wcast-qual',
-    '-Wenum-conversion', '-Waligned-new=all', '-Warray-bounds=2', '-Wdate-time',
-    '-Wredundant-decls', '-Wmismatched-tags', '-Wredundant-tags', '-Wmultichar',
-    '-Wall', '-Wbidi-chars', '-Wsign-promo', '-Wduplicated-cond'
+    '-Wunused-const-variable=2', '-Wconditionally-supported', '-Wwrite-strings',
+    '-Wlarger-than=1073741824', '-Wimplicit-fallthrough=5', '-Wduplicated-cond',
+    '-Wdisabled-optimization', '-Wsuggest-final-methods', '-Wformat-signedness',
+    '-Wtrivial-auto-var-init', '-Wmissing-include-dirs', '-Winfinite-recursion',
+    '-Wdeprecated-copy-dtor', '-Wanalyzer-too-complex', '-Wduplicated-branches',
+    '-Wstrict-null-sentinel', '-Wmissing-declarations', '-Wformat-truncation=2',
+    '-Wmultiple-inheritance', '-Wstack-usage=8388608', '-Winit-self', '-Wsynth',
+    '-Wvirtual-inheritance', '-Wstringop-overflow=4', '-Wcast-qual', '-Wshadow',
+    '-Wsuggest-final-types', '-Woverloaded-virtual', '-Wconversion', '-Walloca',
+    '-Woverlength-strings', '-Wctor-dtor-privacy', '-Wswitch-enum', '-pedantic',
+    '-Wstrict-overflow=2', '-Wcast-align=strict', '-Wfloat-equal', '-Wformat=2',
+    '-Wattribute-alias=2', '-Wformat-overflow=2', '-Winvalid-pch', '-Wvolatile',
+    '-Wshift-overflow=2', '-Warith-conversion', '-Wcatch-value=3', '-Wnoexcept',
+    '-Wuse-after-free=3', '-Wdouble-promotion', '-Wunused-macros', '-Wregister',
+    '-Wsuggest-override', '-Wnull-dereference', '-Wtrampolines', '-Wlogical-op',
+    '-Wnon-virtual-dtor', '-pedantic-errors', '-Wbidi-chars=any', '-Wdate-time',
+    '-Warray-parameter', '-Waligned-new=all', '-Wold-style-cast', '-Wmultichar',
+    '-Wstack-protector', '-Wmissing-braces', '-Warray-bounds=2', '-Walloc-zero',
+    '-Wplacement-new=2', '-Wmismatched-tags', '-Wcomma-subscript', '-Wall',
+    '-Wbidi-chars', '-Wredundant-tags', '-Wenum-conversion', '-Wall',
+    '-Wredundant-decls', '-Wsign-conversion'
 ])
 compile_args = compiler.get_supported_arguments([
     '-nostdinc++', '-fno-exceptions', '-fno-rtti', '-fno-threadsafe-statics'
@@ -104,3 +110,9 @@ executable('tests',
     dependencies: core_dep,
     cpp_args: error_args + compile_args + ['-DCORE_LOG_LEVEL=4'],
     link_args: link_args)
+
+executable('performance', 
+    sources: src_performance,
+    dependencies: core_dep,
+    cpp_args: error_args + compile_args + ['-DCORE_LOG_LEVEL=4'],
+    link_args: link_args)

+ 44 - 0
performance/Main.cpp

@@ -0,0 +1,44 @@
+#include "data/HashMap.h"
+#include "data/ProbingHashMap.h"
+#include "test/Test.h"
+#include "utils/Clock.h"
+
+template<typename Map>
+static void test(const Map& m) {
+    Core::Clock::Nanos nanos = 0;
+    CORE_TEST_ERROR(Core::Clock::getNanos(nanos));
+
+    int sum = 0;
+    for(int i = 0; i < 10000; i++) {
+        for(int k = -5000; k < 5000; k++) {
+            const int* s = m.search(i + k);
+            if(s != nullptr) {
+                sum += *s;
+            }
+        }
+    }
+
+    Core::Clock::Nanos nanos2 = 0;
+    CORE_TEST_ERROR(Core::Clock::getNanos(nanos2));
+
+    CORE_LOG_INFO("# | # ns", sum, nanos2 - nanos);
+}
+
+int main() {
+    Core::HashMap<int, int> m;
+    Core::ProbingHashMap<int, int> m2(1 << 30);
+    for(int i = 0; i < 10000; i++) {
+        CORE_TEST_ERROR(m.add(i, i * i));
+        CORE_TEST_ERROR(m2.add(i, i * i));
+    }
+
+    test(m);
+    test(m);
+    test(m);
+
+    test(m2);
+    test(m2);
+    test(m2);
+
+    return 0;
+}

+ 2 - 0
test/Main.cpp

@@ -17,6 +17,7 @@
 #include "tests/MatrixStackTests.h"
 #include "tests/MatrixTests.h"
 #include "tests/PlaneTests.h"
+#include "tests/ProbingHashMapTests.h"
 #include "tests/QuaternionTests.h"
 #include "tests/RandomTests.h"
 #include "tests/RingBufferTests.h"
@@ -57,6 +58,7 @@ int main() {
     Core::MatrixStackTests::test();
     Core::MatrixTests::test();
     Core::PlaneTests::test();
+    Core::ProbingHashMapTests::test();
     Core::QuaternionTests::test();
     Core::RandomTests::test();
     Core::RingBufferTests::test();

+ 8 - 8
test/Test.h

@@ -86,16 +86,16 @@ namespace Core::Test {
     Core::Test::Internal::checkEqual<Core::RemoveReference<decltype(actual)>>( \
         __FILE__, __LINE__, wanted, actual)
 #define CORE_TEST_STRING(wanted, actual)                                       \
-    Core::Test::Internal::checkString(__FILE__, __LINE__, wanted, actual);
-#define CORE_TEST_FALSE(actual) CORE_TEST_EQUAL(false, actual);
-#define CORE_TEST_TRUE(actual) CORE_TEST_EQUAL(true, actual);
-#define CORE_TEST_ERROR(actual) CORE_TEST_EQUAL(Core::Error::NONE, actual);
-#define CORE_TEST_NULL(actual) CORE_TEST_EQUAL(true, actual == nullptr);
-#define CORE_TEST_NOT_NULL(actual) CORE_TEST_EQUAL(true, actual != nullptr);
+    Core::Test::Internal::checkString(__FILE__, __LINE__, wanted, actual)
+#define CORE_TEST_FALSE(actual) CORE_TEST_EQUAL(false, actual)
+#define CORE_TEST_TRUE(actual) CORE_TEST_EQUAL(true, actual)
+#define CORE_TEST_ERROR(actual) CORE_TEST_EQUAL(Core::Error::NONE, actual)
+#define CORE_TEST_NULL(actual) CORE_TEST_EQUAL(true, actual == nullptr)
+#define CORE_TEST_NOT_NULL(actual) CORE_TEST_EQUAL(true, actual != nullptr)
 #define CORE_TEST_FLOAT(wanted, actual, error)                                 \
-    Core::Test::Internal::checkFloat(__FILE__, __LINE__, wanted, actual, error);
+    Core::Test::Internal::checkFloat(__FILE__, __LINE__, wanted, actual, error)
 #define CORE_TEST_VECTOR(wanted, actual)                                       \
     Core::Test::Internal::checkVector(__FILE__, __LINE__, wanted, actual,      \
-                                      0.0001f);
+                                      0.0001f)
 
 #endif

+ 45 - 13
tests/LinkedListTests.cpp

@@ -24,7 +24,9 @@ static void testAdd() {
     IntList list;
     CORE_TEST_ERROR(list.add(5));
     auto iter = list.begin();
-    CORE_TEST_EQUAL(5, *iter);
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(5, *iter);
+    }
     CORE_TEST_EQUAL(1, list.getLength());
 }
 
@@ -182,20 +184,35 @@ static void testRemoveFirst() {
 
     list.removeFirst();
     auto iter = list.begin();
-    CORE_TEST_EQUAL(3, *iter);
-    CORE_TEST_EQUAL(2, *(++iter));
-    CORE_TEST_EQUAL(1, *(++iter));
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(3, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(2, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(1, *iter);
+    }
     CORE_TEST_EQUAL(3, list.getLength());
 
     list.removeFirst();
     iter = list.begin();
-    CORE_TEST_EQUAL(2, *iter);
-    CORE_TEST_EQUAL(1, *(++iter));
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(2, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(1, *iter);
+    }
     CORE_TEST_EQUAL(2, list.getLength());
 
     list.removeFirst();
     iter = list.begin();
-    CORE_TEST_EQUAL(1, *iter);
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(1, *iter);
+    }
     CORE_TEST_EQUAL(1, list.getLength());
 
     list.removeFirst();
@@ -216,20 +233,35 @@ static void testRemoveLast() {
 
     list.removeLast();
     auto iter = list.begin();
-    CORE_TEST_EQUAL(4, *iter);
-    CORE_TEST_EQUAL(3, *(++iter));
-    CORE_TEST_EQUAL(2, *(++iter));
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(4, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(3, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(2, *iter);
+    }
     CORE_TEST_EQUAL(3, list.getLength());
 
     list.removeLast();
     iter = list.begin();
-    CORE_TEST_EQUAL(4, *iter);
-    CORE_TEST_EQUAL(3, *(++iter));
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(4, *iter);
+        ++iter;
+    }
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(3, *iter);
+    }
     CORE_TEST_EQUAL(2, list.getLength());
 
     list.removeLast();
     iter = list.begin();
-    CORE_TEST_EQUAL(4, *iter);
+    if(CORE_TEST_TRUE(iter != list.end())) {
+        CORE_TEST_EQUAL(4, *iter);
+    }
     CORE_TEST_EQUAL(1, list.getLength());
 
     list.removeLast();

+ 6 - 4
tests/ListTests.cpp

@@ -7,8 +7,9 @@ using IntList = Core::List<int>;
 
 static void testAdd() {
     IntList list;
-    CORE_TEST_ERROR(list.add(5));
-    CORE_TEST_EQUAL(5, list[0]);
+    if(CORE_TEST_ERROR(list.add(5))) {
+        CORE_TEST_EQUAL(5, list[0]);
+    }
     CORE_TEST_EQUAL(1, list.getLength());
 }
 
@@ -25,8 +26,9 @@ static void testMultipleAdd() {
 
 static void testAddReplace() {
     IntList list;
-    CORE_TEST_ERROR(list.add(5));
-    list[0] = 3;
+    if(CORE_TEST_ERROR(list.add(5))) {
+        list[0] = 3;
+    }
     CORE_TEST_EQUAL(3, list[0]);
 }
 

+ 336 - 0
tests/ProbingHashMapTests.cpp

@@ -0,0 +1,336 @@
+#include "tests/ProbingHashMapTests.h"
+
+#include <stdio.h>
+
+#include "data/ProbingHashMap.h"
+#include "test/Test.h"
+
+using IntMap = Core::ProbingHashMap<int, int>;
+
+static void testAdd() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    int* value = map.search(5);
+    CORE_TEST_NOT_NULL(value);
+    if(value != nullptr) {
+        CORE_TEST_EQUAL(4, *value);
+    }
+}
+
+static void testMultipleAdd() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(10, 3));
+    CORE_TEST_ERROR(map.add(15, 2));
+    CORE_TEST_TRUE(map.contains(5));
+    CORE_TEST_TRUE(map.contains(10));
+    CORE_TEST_TRUE(map.contains(15));
+    int* a = map.search(5);
+    int* b = map.search(10);
+    int* c = map.search(15);
+    CORE_TEST_NOT_NULL(a);
+    CORE_TEST_NOT_NULL(b);
+    CORE_TEST_NOT_NULL(c);
+    if(a != nullptr && b != nullptr && c != nullptr) {
+        CORE_TEST_EQUAL(4, *a);
+        CORE_TEST_EQUAL(3, *b);
+        CORE_TEST_EQUAL(2, *c);
+    }
+}
+
+/*static void testSearch() {
+    IntMap map(1 << 30);
+    CORE_TEST_NULL(map.search(6));
+}
+
+static void testAddReplace() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(5, 10));
+    CORE_TEST_TRUE(map.contains(5));
+    int* a = map.search(5);
+    CORE_TEST_NOT_NULL(a);
+    if(a != nullptr) {
+        CORE_TEST_EQUAL(10, *a);
+    }
+}
+
+static void testClear() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(4, 10));
+    map.clear();
+    CORE_TEST_FALSE(map.contains(5));
+    CORE_TEST_FALSE(map.contains(4));
+}
+
+static void testOverflow() {
+    IntMap map(1 << 30);
+    for(int i = 0; i < 100000; i++) {
+        CORE_TEST_ERROR(map.add(i, i));
+    }
+    for(int i = 0; i < 100000; i++) {
+        CORE_TEST_TRUE(map.contains(i));
+    }
+}
+
+struct A final {
+    int a;
+    int b;
+
+    A(int a_, int b_) : a(a_), b(b_) {
+    }
+
+    // none of these should be needed for the hashmap
+    A(const A&) = delete;
+    A(A&&) = delete;
+    A& operator=(const A&) = delete;
+    A& operator=(A&&) = delete;
+
+    bool operator==(const A& other) const {
+        return a == other.a && b == other.b;
+    }
+
+    template<typename String>
+    check_return Core::Error toString(String& s) const {
+        CORE_RETURN_ERROR(s.append("A("));
+        CORE_RETURN_ERROR(s.append(a));
+        CORE_RETURN_ERROR(s.append(", "));
+        CORE_RETURN_ERROR(s.append(b));
+        CORE_RETURN_ERROR(s.append(")"));
+        return Core::Error::NONE;
+    }
+};
+
+static void testEmplace() {
+    Core::HashMap<int, A> map;
+
+    A* ar = nullptr;
+    CORE_TEST_ERROR(map.tryEmplace(ar, 0, 3, 4));
+    CORE_TEST_ERROR(map.tryEmplace(ar, 3, 4, 5));
+    CORE_TEST_ERROR(map.tryEmplace(ar, 20, 5, 6));
+    CORE_TEST_EQUAL(Core::Error::EXISTING_KEY, map.tryEmplace(ar, 3, 6, 7));
+    CORE_TEST_EQUAL(Core::Error::EXISTING_KEY, map.tryEmplace(ar, 20, 7, 8));
+
+    A* a = map.search(0);
+    A* b = map.search(3);
+    A* c = map.search(20);
+
+    CORE_TEST_NOT_NULL(a);
+    CORE_TEST_NOT_NULL(b);
+    CORE_TEST_NOT_NULL(c);
+
+    if(a != nullptr && b != nullptr && c != nullptr) {
+        CORE_TEST_EQUAL(A(3, 4), *a);
+        CORE_TEST_EQUAL(A(4, 5), *b);
+        CORE_TEST_EQUAL(A(5, 6), *c);
+    }
+}
+
+static void testToString1() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_ERROR(map.add(2, 4));
+    CORE_TEST_ERROR(map.add(3, 5));
+    CORE_TEST_STRING("[1 = 3, 2 = 4, 3 = 5]", map);
+}
+
+static void testToString2() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_STRING("[1 = 3]", map);
+}
+
+static void testToString3() {
+    IntMap map(1 << 30);
+    CORE_TEST_STRING("[]", map);
+}
+
+static void testCopy() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_ERROR(map.add(2, 4));
+    CORE_TEST_ERROR(map.add(3, 5));
+    IntMap copy;
+    CORE_TEST_ERROR(copy.copyFrom(map));
+
+    int* a[6] = {map.search(1),  map.search(2),  map.search(3),
+                 copy.search(1), copy.search(2), copy.search(3)};
+    for(int i = 0; i < 3; i++) {
+        CORE_TEST_NOT_NULL(a[i]);
+        CORE_TEST_NOT_NULL(a[i + 3]);
+        if(a[i] != nullptr && a[i + 3] != nullptr) {
+            CORE_TEST_EQUAL(*(a[i]), *(a[i + 3]));
+        }
+    }
+}
+
+static void testMove() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_ERROR(map.add(2, 4));
+    CORE_TEST_ERROR(map.add(3, 5));
+    IntMap move(Core::move(map));
+
+    int* a = move.search(1);
+    int* b = move.search(2);
+    int* c = move.search(3);
+
+    CORE_TEST_NOT_NULL(a);
+    CORE_TEST_NOT_NULL(b);
+    CORE_TEST_NOT_NULL(c);
+
+    if(a != nullptr && b != nullptr && c != nullptr) {
+        CORE_TEST_EQUAL(3, *a);
+        CORE_TEST_EQUAL(4, *b);
+        CORE_TEST_EQUAL(5, *c);
+    }
+}
+
+static void testMoveAssignment() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_ERROR(map.add(2, 4));
+    CORE_TEST_ERROR(map.add(3, 5));
+
+    IntMap move;
+    move = Core::move(map);
+
+    int* a = move.search(1);
+    int* b = move.search(2);
+    int* c = move.search(3);
+
+    CORE_TEST_NOT_NULL(a);
+    CORE_TEST_NOT_NULL(b);
+    CORE_TEST_NOT_NULL(c);
+
+    if(a != nullptr && b != nullptr && c != nullptr) {
+        CORE_TEST_EQUAL(3, *a);
+        CORE_TEST_EQUAL(4, *b);
+        CORE_TEST_EQUAL(5, *c);
+    }
+}
+
+static void testRemove() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(1, 3));
+    CORE_TEST_ERROR(map.add(2, 4));
+    CORE_TEST_ERROR(map.add(3, 5));
+
+    CORE_TEST_ERROR(map.remove(2));
+    CORE_TEST_EQUAL(Core::Error::NOT_FOUND, map.remove(7));
+
+    int* a = map.search(1);
+    int* b = map.search(2);
+    int* c = map.search(3);
+
+    CORE_TEST_NOT_NULL(a);
+    CORE_TEST_NULL(b);
+    CORE_TEST_NOT_NULL(c);
+
+    if(a != nullptr && c != nullptr) {
+        CORE_TEST_EQUAL(3, *a);
+        CORE_TEST_EQUAL(5, *c);
+    }
+}
+
+static void testEntryForEach() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(10, 3));
+    CORE_TEST_ERROR(map.add(15, 2));
+
+    int counter = 0;
+    for(auto& entry : map.entries()) {
+        counter += entry.getKey() + entry.value;
+    }
+    CORE_TEST_EQUAL(39, counter);
+
+    const IntMap& cmap = map;
+    counter = 0;
+    for(const auto& entry : cmap.entries()) {
+        counter += entry.getKey() + entry.value;
+    }
+    CORE_TEST_EQUAL(39, counter);
+}
+
+static void testKeyForEach() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(10, 3));
+    CORE_TEST_ERROR(map.add(15, 2));
+
+    int counter = 0;
+    for(const int& key : map.keys()) {
+        counter += key;
+    }
+    CORE_TEST_EQUAL(30, counter);
+
+    const IntMap& cmap = map;
+    counter = 0;
+    for(const int& key : cmap.keys()) {
+        counter += key;
+    }
+    CORE_TEST_EQUAL(30, counter);
+}
+
+static void testValueForEach() {
+    IntMap map(1 << 30);
+    CORE_TEST_ERROR(map.add(5, 4));
+    CORE_TEST_ERROR(map.add(10, 3));
+    CORE_TEST_ERROR(map.add(15, 2));
+
+    int counter = 0;
+    for(int& value : map.values()) {
+        counter += value;
+    }
+    CORE_TEST_EQUAL(9, counter);
+
+    const IntMap& cmap = map;
+    counter = 0;
+    for(const int& value : cmap.values()) {
+        counter += value;
+    }
+    CORE_TEST_EQUAL(9, counter);
+}
+
+template<typename T>
+static void testType() {
+    Core::HashMap<T, int> m;
+    CORE_TEST_ERROR(m.add(T(), 3));
+}
+
+static void testTypes() {
+    testType<char>();
+    testType<signed char>();
+    testType<signed short>();
+    testType<signed int>();
+    testType<signed long>();
+    testType<signed long long>();
+    testType<unsigned char>();
+    testType<unsigned short>();
+    testType<unsigned int>();
+    testType<unsigned long>();
+    testType<unsigned long long>();
+}*/
+
+void Core::ProbingHashMapTests::test() {
+    testAdd();
+    testMultipleAdd();
+    // testSearch();
+    // testAddReplace();
+    // testClear();
+    // testOverflow();
+    // testEmplace();
+    // testToString1();
+    // testToString2();
+    // testToString3();
+    // testCopy();
+    // testMove();
+    // testMoveAssignment();
+    // testRemove();
+    // testEntryForEach();
+    // testKeyForEach();
+    // testValueForEach();
+    // testTypes();
+}

+ 8 - 0
tests/ProbingHashMapTests.h

@@ -0,0 +1,8 @@
+#ifndef CORE_PROBING_HASHMAP_TESTS_H
+#define CORE_PROBING_HASHMAP_TESTS_H
+
+namespace Core::ProbingHashMapTests {
+    void test();
+}
+
+#endif

+ 1 - 1
tests/ThreadTests.cpp

@@ -17,7 +17,7 @@ static int run(void*) {
 static void testStart() {
     runDone = 0;
     Core::Thread::Id id = Core::Thread::INVALID_ID;
-    CORE_TEST_ERROR(Core::Thread::start(id, run, nullptr))
+    CORE_TEST_ERROR(Core::Thread::start(id, run, nullptr));
     int returnValue = 0;
     CORE_TEST_ERROR(Core::Thread::join(id, &returnValue));
     CORE_TEST_EQUAL(1, runDone);

+ 1 - 1
utils/HashCode.cpp

@@ -3,7 +3,7 @@
 #include "utils/Utility.h"
 
 template<typename T>
-u32 hashNumber(T t) {
+static u32 hashNumber(T t) {
     constexpr u32 L = sizeof(T) / 4 + (sizeof(T) % 4 != 0);
     u32 parts[L];
     Core::memorySet(parts, 0, sizeof(parts));