EliasFarhan/NekoEngine

Feedback sur l'implémentation d'une map?

Closed this issue · 5 comments

Séb et moi avons fait un premier jet d'une implémentation d'une hash map. Pourrait-on avoir du feedback svp?

#pragma once

#include <engine/custom_allocator.h>
#include <vector> // TODO: replace this with neko's own container.
#include <xxhash.hpp>

namespace neko
{

template<typename Key, typename Value>
class Map
{
public:
    // Constructor / destructor.
    Map(Allocator* allocator, const size_t sizeInBytes)
    {
        allocator = nullptr; // TODO: Pass this allocator to neko's container.

        pairs_.resize(sizeInBytes / sizeof(Value));
        for (auto& pair : pairs_)
        {
            pair = std::pair<xxh::hash_t<64>, Value>(0, Value());
        }
    }

    // Overloads.
    inline Map<Key, Value>& operator=(Map<Key, Value>&& other) noexcept
    {
        return *this;
    }

    inline bool IsEmpty()
    {
        return count_ == 0;
    }

    inline size_t Count()
    {
        return count_;
    }

    inline size_t Capacity()
    {
        return pairs_.capacity();
    }

    bool Add(const Key key, const Value value) const
    {
        // TODO@Seb: matchCount that key doesn't exist already in the map! Otherwise 1 key may have multiple values!

        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = pairs_.capacity();
        for (size_t i = 0; i < len; i++)
        {
            if (pairs_[i].first == 0)
            {
                pairs_[i].first = hash;
                pairs_[i].second = value;
                return true;
            }
        }
        return false;
    }

    bool Remove(const Key key) const
    {
        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = pairs_.capacity();
        for (int i = 0; i < len; i++)
        {
            if (pairs_[i].first == hash)
            {
                pairs_[i].first = 0;
                pairs_[i].second = Value();
                return true;
            }
        }
        return false;
    }

    void Clear() const
    {
        for (auto& pair : pairs_)
        {
            pair = std::pair<xxh::hash_t<64>, Value>(0, Value());
        }
    }

    bool Swap(const Key a, const Key b) const
    {
        if (a == b) return true;

        const xxh::hash_t<64> hash0 = xxh::xxhash<64>(&a, sizeof(Key));
        const xxh::hash_t<64> hash1 = xxh::xxhash<64>(&b, sizeof(Key));
        const size_t len = count_;

        size_t index0 = 0;
        size_t index1 = 0;
        size_t i = 0;
        bool firstMatch = false;

        while (i++ < len)
        {
            auto hash = pairs_[i].first;
            if (hash == hash0 || hash == hash1)
            {
                if (!firstMatch)
                {
                    index0 = i;
                    firstMatch = true;
                }
                else
                {
                    index1 = i;
                    goto RETURN;
                }
            }
        }
        return false; // Traversed whole map without finding both keys.

        RETURN:
        const auto temp = pairs_[index0].second;
        pairs_[index0].second = pairs_[index1].second;
        pairs_[index1].second = temp;
        return true;
    }

    Value& FindValue(const Key key) const
    {
        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = count_;

        for (size_t i = 0; i < len; i++)
        {
            if (pairs_[i].first == hash)
            {
                return pairs_[i].second;
            }
        }
        return Value();
    }

private:
    std::vector<std::pair<xxh::hash_t<64>, Value>> pairs_; // TODO: Replace this with neko's container when it's ready.
    size_t count_ = 0;
};

}// !neko

Merci!

J'ai debuggé le code entre temps:

#pragma once

#include <engine/custom_allocator.h>
#include <vector> // TODO: replace this with neko's own container.
#include <xxhash.hpp>

namespace neko
{

template<typename Key, typename Value>
class Map
{
public:
    // Constructor / destructor.
    Map(Allocator* allocator, const size_t sizeInBytes)
    {
        allocator = nullptr; // TODO: Pass this allocator to neko's container.

        pairs_.resize(sizeInBytes / sizeof(Value));
        for (auto& pair : pairs_)
        {
            pair = std::pair<xxh::hash_t<64>, Value>(0, Value());
        }
    }

    // Overloads.
    inline Map<Key, Value>& operator=(Map<Key, Value>&& other) noexcept
    {
        return *this;
    }

    inline bool IsEmpty()
    {
        return count_ == 0;
    }

    inline size_t Count()
    {
        return count_;
    }

    inline size_t Capacity()
    {
        return pairs_.capacity();
    }

    bool Add(const Key key, const Value value)
    {
        // TODO@Seb: check that key doesn't exist already in the map! Otherwise 1 key may have multiple values!

        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = pairs_.capacity();
        for (size_t i = 0; i < len; i++)
        {
            if (pairs_[i].first == 0)
            {
                pairs_[i].first = hash;
                pairs_[i].second = value;
                count_++;
                return true;
            }
        }
        return false;
    }

    bool Remove(const Key key)
    {
        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = pairs_.capacity();
        for (int i = 0; i < len; i++)
        {
            if (pairs_[i].first == hash)
            {
                pairs_[i].first = 0;
                pairs_[i].second = Value();
                count_--;
                return true;
            }
        }
        return false;
    }

    void Clear()
    {
        for (auto& pair : pairs_)
        {
            pair = std::pair<xxh::hash_t<64>, Value>(0, Value());
        }
        count_ = 0;
    }

    bool Swap(const Key a, const Key b)
    {
        if (a == b || count_ < 2) return false;

        const xxh::hash_t<64> hash0 = xxh::xxhash<64>(&a, sizeof(Key));
        const xxh::hash_t<64> hash1 = xxh::xxhash<64>(&b, sizeof(Key));
        const size_t len = count_;

        size_t index0 = 0;
        size_t index1 = 0;
        size_t i = 0;
        bool firstMatch = false;

        do
        {
            auto hash = pairs_[i].first;
            if (hash == hash0 || hash == hash1)
            {
                if (!firstMatch)
                {
                    index0 = i;
                    firstMatch = true;
                }
                else
                {
                    index1 = i;
                    goto RETURN;
                }
            }
        } while (i++ < len);
        return false; // Traversed whole map without finding both keys.

        RETURN:
        const auto temp = pairs_[index0].second;
        pairs_[index0].second = pairs_[index1].second;
        pairs_[index1].second = temp;
        return true;
    }

    Value FindValue(const Key key) const
    {
        // TODO@Seb: Have to traverse whole vector regardless of count_ if there are holes in the memory layout!

        const xxh::hash_t<64> hash = xxh::xxhash<64>(&key, sizeof(Key));
        const size_t len = pairs_.size(); // TODO@Seb: this...

        for (size_t i = 0; i < len; i++)
        {
            if (pairs_[i].first == hash)
            {
                return pairs_[i].second;
            }
        }
        return 0;
    }

private:
    std::vector<std::pair<xxh::hash_t<64>, Value>> pairs_; // TODO: Replace this with neko's container when it's ready.
    size_t count_ = 0;
};

}// !neko

Okay, c'est un VectorMap votre classe hein! Vous pouvez utiliser using KeyValuePair = std::pair<..., ...> aussi pour cleaner un peu.

Et il faut tester tout ça si c'est pas fait. Benchmarker les fonctions usuelles (insert, get_value, ...) par rapport à std::map aussi.

T'avais bien dit de commençer par une VectorMap non? Quelles autres implémentations est-ce qu'on doit faire?
C'est quoi la différence entre une vector map et une hash map?

Et on a un test pour s'assurer que tout fonctionne, mais je rajoute un benchmark pour comparer notre map avec celle du std alors.

Benchmark de FindValue():


Benchmark Time CPU Iterations

BM_StdMap/2 214 ns
BM_StdMap/8 1295 ns
BM_StdMap/64 14494 ns
BM_StdMap/512 144762 ns
BM_NekoMap/2 292 ns
BM_NekoMap/8 1320 ns
BM_NekoMap/64 26889 ns
BM_NekoMap/512 1227872 ns

.....ouch