очистка nullptr в отношении один-ко-многим, использующему нестандартный слабый указатель - PullRequest
7 голосов
/ 29 апреля 2019

У меня есть класс карты «один ко многим» - MyMap1N<WeakPtr_Parent,WeakPtr_Children>.
По замыслу предполагается хранить слабые указатели игрового экземпляра.

Грубо говоря, это называется: -

MyMap1N<WeakPtr<Room>,WeakPtr<RigidBody>> map;
WeakPtr<Room> room=create<Room>();
WeakPtr<RigidBody> body=create<RigidBody>();
map.add(room,body);
MyArray<WeakPtr<RigidBody>> bodys=map.getAllChildren(room);

Профилируя, я обнаружил, что std::unordered_map слишком медленно.
Таким образом, мне пришлось искать другой способ его реализации.

Я решил создать массив (вместо unordered_map) в Room.
Чтобы увеличить скорость запроса, я также добавляю indexInArray для хранения около каждого экземпляра RigidBody (см. Изображение ниже).

С помощью этого indexInArray можно выполнить операции add(room,body) и remove(room,body) get O(1) и гарантировать, что каждый слот Room::bodys занят.

enter image description here

Вопрос

Проблема возникает при удалении некоторых экземпляров child (RigidBody).
MyMap1N не может даже знать это.

enter image description here

Как очистить MyMap1N при удалении некоторых экземпляров RigidBody?

Примечание: (доступные инструменты / ограничение)

  • К счастью, в моем случае проверка "10 * * nullptr" очень дешева.
  • Каждый экземпляр имеет свой уникальный int ID.
    Идентификатор выполняет разделение для каждого типа, и значение идентификатора является низким (потому что я его перезаписываю).
  • Я использую многопоточность.
  • (Редактировать: уточнить) Есть много MyMap1N<Something,Something>, которые разбросаны по многим System-like классам.
    Таким образом, жестко кодировать его очень сложно: -

    rigidBody->destroy() ===> {     
            SystemA::mapRoomBody::removeParent(rigidBody) ;
            SystemA::mapCatBody::removeParent(rigidBody) ;
            SystemB::mapBodyDog::removeAllChildren(rigidBody) ;
    }  //: Cat and Dog denotes some arbitrary GameObject-type class
    

Мое плохое решение

Раствор 1

Я регистрирую каждые экземпляров MyMap1N в центральном местоположении автоматически.

Если RigidBody удален, центральная система будет обратный вызов на каждый связанный MyMap1N.

(Чтобы определить, связано ли MyMap1N,
Я использовал некоторые шаблоны магии, такие как MyMap1N::Type_Parent и MyMap1N::Type_Children.)

rigidBody->destroy()   
    ===> central->kill(RigidBody*) 
        ===> MyMap1N<WeakPtr<Room>,WeakPtr<RigidBody>>::removeParent(RigidBody*) 
              ... and many other related instances of MyMap1N

Работает, но очень медленно.
Я полагаю, что кэш пропускает причину (не уверен)

Решение 2 (моя старая версия)

Всякий раз, когда пользователь хочет удалить RigidBody, просто помечает его.
В конце временного шага сделайте то же самое, что и обходной путь 1.
Это быстрее. Возможно, это потому, что компьютер любит дозировать. (например, меньшая стоимость)
Тем не менее, он все еще использует процессор около 10-20% всей игры.

Решение 3 (в настоящее время используется)

Если RigidBody удален, ничего не делать.
Однако когда я запрашиваю add(room,body)/remove(room,body)/getAllChildren(room)/getParent(body), я должен проверить, является ли WeakPtr<>==nullptr.

Это быстро. При удалении нет никаких затрат, и каждый запрос также выполняется быстро.

Недостаток в том, что массив Room::bodys растет навсегда
потому что Room::Bodys постепенно заполняется X (Occupied but the object was deleted).
Моя программа генерирует ошибку assert-memory-fail на 200-м временном шаге.

enter image description here

Решение 4

Я подумываю об использовании решения 3,
но также создаю новую функцию MyMap1N::periodicCleanUp, чтобы удалить все X, т.е. перепаковать ее.

Функция должна вызываться периодически, возможно, один раз каждые 10 шагов.
(как большой день уборки)

Я чувствую, что это взлом, и в значительной степени основанный на пользовательской настройке (т.е. субъективная настройка).

1 Ответ

1 голос
/ 01 мая 2019

Из того, что было собрано из вопроса и комментариев, представляется несколько жизнеспособных решений.

Раствор 1

Первое возможное решение, на которое другие указывали в комментариях, - это использовать свободный индексный слот перед добавлением в массив. Это будет включать в себя каждый Room или объект, содержащий массив RigidBody, чтобы иметь список свободных индексов, std::forward_list или std::vector было бы хорошо для этого. Затем вы можете добавить RigidBody, сначала проверив, есть ли в списке доступный слот. Если есть, вы извлекаете этот индекс из списка, в противном случае вы добавляете его в массив. Удаление RigidBody просто включает в себя помещение этого освобожденного индекса в список доступных слотов. Теперь это решение потребует, чтобы каждый RigidBody содержал список родительских и индексных пар. Таким образом, когда RigidBody уничтожен, вы просто уведомляете каждого родителя, чтобы освободить индекс, который использовал объект.

Преимущества

  • Возможно, это немного странно для реализации.
  • Добавление и удаление O(1).
  • Скорость итерации обычно хорошая.

Недостатки

  • Используется приличное количество памяти.
  • Массив будет расти.
  • Приходится использовать уникальный ключ для каждого родителя.

Раствор 2

Существует также другой аналогичный тип решения, который обсуждался в комментариях. Однако вместо RigidBody, имеющего несколько индексов для каждого родителя, он имеет один уникальный идентификатор, который действует как индекс. Этот уникальный идентификатор должен иметь известный диапазон минимальных и максимальных значений. Затем каждый родитель выделит достаточно места для размещения максимального количества идентификаторов и RigidBodies. Уничтожить и удалить RigidBody очень просто, поскольку вам нужно просто передать идентификатор / индекс каждому зарегистрированному родителю. Кроме того, вы можете использовать список для отслеживания бесплатных идентификаторов.

Преимущества

  • Массив не будет расти во время выполнения.
  • Добавление и удаление O(1).
  • Меньше ключей и индексов.
  • Один и тот же ключ / индекс для всех родителей.
  • Отлично, если массив будет в основном заполнен.

Недостатки

  • Использует много памяти.
  • Итерация будет неэффективной, если массив в основном пуст.

Решение 3

Идея периодической очистки, которую вы предложили, может сработать. Однако вполне вероятно, что очистка всех массивов за один раз может занять много времени. Следовательно, возможной настройкой будет частичная очистка массива в конце каждого временного шага. Эта корректировка потребует от вас сохранения индекса того, где вы остановились в последний раз. На что вы будете использовать этот индекс для продолжения очистки разделов массива. Как только массив будет полностью очищен, вы можете сбросить этот индекс до 0 и начать заново. Это решение и корректировка будут работать только в том случае, если скорость удаления тел обычно превышает скорость добавления тел.

Преимущества

  • Простота реализации.
  • Простота настройки и настройки.

Недостатки

  • Может произойти сбой в зависимости от скорости добавления и удаления элементов.
  • Может использовать больше памяти, чем необходимо.

Решение 4

Другое решение будет включать использование адреса или идентификатора твердого тела для хеширования или его в массив векторов. Этот массив векторов может быть выполнен с использованием простого числа, чтобы действовать как размер массива. Затем мы можем использовать идентификатор или адрес RigidBodies и по модулю с размером массива поместить его в вектор. Это делает стирание быстрее, чем нормальный вектор. Кроме того, он использует меньше памяти, чем массивный статический массив слотов. Итерация по этой структуре будет включать в себя итерацию по каждому сегменту / вектору. Или вы можете создать собственный итератор, который сделает это за вас.

Базовая реализация структуры

namespace {
    template<typename Int>
    constexpr bool isPrime(Int num, Int test = 2) {
        return (test * test > num ? true : (num % test == 0 ? false : isPrime(num, test + 1)));
    }
    //Buckets must be a size
    template<typename data_t, std::size_t PRIME_SIZE, typename = typename std::enable_if<isPrime(PRIME_SIZE)>::type>
    class BucketVector
    {
    public:
        constexpr static auto SIZE = PRIME_SIZE;
        template<bool is_const>
        using BucketIteratorBase = typename  std::iterator<std::bidirectional_iterator_tag, typename std::conditional<is_const, const data_t, data_t>::type>;
        using uint_t = std::uintptr_t;
        using BucketType = std::vector<data_t>;
        template<bool is_const>
        class BucketIterator : public BucketIteratorBase<is_const> {
        public:
            using Base = BucketIteratorBase<is_const>;
            using BucketOwner = BucketVector<data_t, PRIME_SIZE>;
            using typename Base::pointer;
            using typename Base::reference;
            using typename Base::value_type;
            friend class BucketIterator<!is_const>;
            std::size_t m_bucket;
            pointer m_value;
            BucketOwner* m_owner;
        public:
            BucketIterator(std::size_t bucket, pointer value, BucketOwner* owner)
                : m_bucket(bucket),
                m_value(value),
                m_owner(owner) {
                //validateIterator();
            }
            ~BucketIterator() {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator(const BucketIterator<value>& iterator)
                : m_bucket(iterator.m_bucket),
                m_value(iterator.m_value),
                m_owner(iterator.m_owner) {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator(BucketIterator<value>&& iterator)
                : m_bucket(std::move(iterator.m_bucket)),
                m_value(std::move(iterator.m_value)),
                m_owner(std::move(iterator.m_owner)) {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator& operator=(BucketIterator<value>&& iterator) {
                m_bucket = std::move(iterator.m_bucket);
                m_value = std::move(iterator.m_value);
                m_owner = std::move(iterator.m_owner);
                return *this;
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator& operator=(const BucketIterator<value>& iterator) {
                m_bucket = iterator.m_bucket;
                m_value = iterator.m_value;
                m_owner = iterator.m_owner;
                return *this;
            }
            BucketIterator& operator++() {
                ++m_value;
                forwardValidate();
                return *this;
            }
            BucketIterator operator++(int) {
                BucketIterator copy(*this);
                ++(*this);
                return copy;
            }
            BucketIterator& operator--() {
                backwardValidate();
                --m_value;
                return *this;
            }
            BucketIterator operator--(int) {
                BucketIterator copy(*this);
                --(*this);
                return copy;
            }
            reference operator*() const {
                return *m_value;
            }
            pointer operator->() const {
                return m_value;
            }
            template<bool value>
            bool operator==(const BucketIterator<value>& iterator) const {
                return m_bucket == iterator.m_bucket && m_owner == iterator.m_owner && m_value == iterator.m_value;
            }
            template<bool value>
            bool operator!=(const BucketIterator<value>& iterator) const {
                return !(this->operator==(iterator));
            }
            BucketOwner* getSystem() const {
                return m_owner;
            }
            inline void backwardValidate() {
                while (m_value == m_owner->m_buckets[m_bucket].data() && m_bucket != 0) {
                    --m_bucket;
                    m_value = m_owner->m_buckets[m_bucket].data() + m_owner->m_buckets[m_bucket].size();
                }
            }
            inline void forwardValidate() {
                while (m_value == (m_owner->m_buckets[m_bucket].data() + m_owner->m_buckets[m_bucket].size()) && m_bucket != SIZE - 1) {
                    m_value = m_owner->m_buckets[++m_bucket].data();
                }
            }
        };
        using iterator = BucketIterator<false>;
        using const_iterator = BucketIterator<true>;
        friend class BucketIterator<false>;
        friend class BucketIterator<true>;
    private:
        std::array<BucketType, SIZE> m_buckets;
        std::size_t m_size;
    public:
        BucketVector()
            : m_size(0) {
        }
        ~BucketVector() {
        }
        BucketVector(const BucketVector&) = default;
        BucketVector(BucketVector&&) = default;
        BucketVector& operator=(const BucketVector&) = default;
        BucketVector& operator=(BucketVector&&) = default;
        data_t& operator[](std::size_t index) {
            const auto bucketIndex = findBucketIndex(index);
            return m_buckets[bucketIndex.first][bucketIndex.second];
        }
        const data_t& operator[](std::size_t index) const {
            return static_cast<BucketVector*>(this)->operator[](index);
        }
        data_t& at(std::size_t index) {
            if (index >= m_size) {
                throw std::out_of_range("BucketVector::at index out of range");
            }
            return this->operator[](index);
        }
        const data_t& at(std::size_t index) const {
            return static_cast<BucketVector*>(this)->at(index);
        }
        void erase(const_iterator iter) {
            auto& bucket = m_buckets[iter.m_bucket];
            std::size_t index = iter.m_value - bucket.data();
            bucket[index] = bucket.back();
            bucket.pop_back();
            --m_size;
        }
        void push_back(uint_t id, const data_t& data) {
            const auto slot = get_slot(id);
            m_buckets[slot].push_back(data);
            ++m_size;
        }
        void push_back(uint_t id, data_t&& data) {
            const auto slot = get_slot(id);
            m_buckets[slot].push_back(std::move(data));
            ++m_size;
        }
        template<typename... args>
        void emplace_back(uint_t id, args&&... parameters) {
            const auto slot = get_slot(id);
            m_buckets[slot].emplace_back(std::forward<args>(parameters)...);
            ++m_size;
        }

        void pop_back(uint_t index) {
            const auto slot = get_slot(index);
            m_buckets[slot].pop_back();
            --m_size;
        }
        void pop_front(uint_t index) {
            const auto slot = get_slot(index);
            m_buckets[slot].pop_front();
            --m_size;
        }
        void reserve(std::size_t size) {
            const std::size_t slotSize = size / SIZE + 1;
            for (auto& bucket : m_buckets) {
                bucket.reserve(slotSize);
            }
        }
        void clear() {
            for (auto& bucket : m_buckets) {
                bucket.clear();
            }
        }
        bool empty() const {
            return m_size != 0;
        }
        std::size_t size() const {
            return m_size;
        }
        iterator find(uint_t index, const data_t& value) {
            const std::size_t slot = get_slot(index);
            auto& bucket = m_buckets[slot];
            for (auto it = bucket.begin(), end = bucket.end(); it != end; ++it) {
                if (*it == value) {
                    return { slot, &(*it), this };
                }
            }
            return end();
        }
        template<typename fn_t>
        iterator find(uint_t index, const fn_t& fn) {
            const std::size_t slot = get_slot(index);
            auto& bucket = m_buckets[slot];
            for (auto it = bucket.begin(), end = bucket.end(); it != end; ++it) {
                if (fn(*it)) {
                    return { slot, &(*it), this };
                }
            }
            return end();
        }
        const_iterator find(uint_t index, const data_t& value) const {
            return cfind(index, value);
        }
        const_iterator cfind(uint_t index, const data_t& value) const {
            return static_cast<BucketVector*>(this)->find(index, value);
        }
        iterator begin(uint_t index = 0) {
            auto bucketIndex = findBucketIndex(index);
            iterator it{ bucketIndex.first, m_buckets[bucketIndex.first].data() + bucketIndex.second, this };
            it.forwardValidate();
            return it;
        }
        iterator end(uint_t index = 0) {
            iterator it{ SIZE - 1, m_buckets.back().data() + m_buckets.back().size(), this };
            return it;
        }
        const_iterator begin(uint_t index = 0) const {
            auto bucketIndex = findBucketIndex(index);
            const_iterator it{ bucketIndex.first, m_buckets[bucketIndex.first].data() + bucketIndex.second, this };
            it.forwardValidate();
            return it;
        }
        const_iterator end(uint_t index = 0) const {
            const_iterator it{ SIZE - 1, m_buckets.back().data() + m_buckets.back().size(), this };
            return it;
        }
        std::size_t get_slot(uint_t id) {
            return id % SIZE;
        }
    private:
        inline std::pair<std::size_t, std::size_t> findBucketIndex(std::size_t index) {
            std::size_t bucket = 0;
            std::size_t count = 0;
            while (index >= m_buckets[bucket].size() + count) {
                count += m_buckets[bucket].size();
                ++bucket;
            }
            return { bucket, index - count };
        }
    };
}

Преимущества

  • Добавление составляет O(1).
  • Используйте меньше памяти, чем в решениях 1 и 2.
  • Может использоваться для быстрого определения, принадлежит ли RigidBody родительскому элементу.
  • Стирание выполняется быстро для размера вектора, который вы
  • Итерация быстрее, чем в решениях 1 и 2, если массив пуст более чем на 50%.

Недостатки

  • Стираниебыстро, но не так быстро, как в решениях 1 и 2.
  • Векторы будут расти.
  • Итерация медленнее, чем в решениях 1 и 2, если массив заполнен более чем на 50%.

Basic Benchmark Program

Вы можете использовать эту программу для проверки различных входных данных, таких как размер и количество значений, которые нужно удалить, чтобы увидеть производительность.

#include <chrono>
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <set>
#include <iomanip>
#include <unordered_set>
#include <array>
#include <vector>
#include <iterator>
#include <type_traits>


template<typename mclock_t = typename std::conditional<std::chrono::high_resolution_clock::is_steady, std::chrono::high_resolution_clock, std::chrono::steady_clock>::type>
class Benchmarker {
public:
    using ClockType = mclock_t;
    using TimePoint = std::chrono::time_point<ClockType>;
private:
    TimePoint m_start;
    TimePoint m_end;
    bool m_running;
public:
    Benchmarker(bool run = false) {
        m_running = run;

        if (m_running) {
            start();
        }
    }

    Benchmarker& start() {
        m_start = ClockType::now();
        m_running = true;

        return *this;
    }

    Benchmarker& stop() {
        m_end = ClockType::now();
        m_running = false;

        return *this;
    }

    template<typename T = std::chrono::microseconds>
    Benchmarker& printDuration(std::ostream& out) {
        out << std::chrono::duration_cast<T>(m_end - m_start).count();

        return *this;
    }

    template<typename T = std::chrono::microseconds>
    long long getDurationCount() {
        return std::chrono::duration_cast<T>(m_end - m_start).count();
    }

    friend std::ostream& operator<<(std::ostream& out, Benchmarker& benchmarker) {
        out << std::chrono::duration_cast<std::chrono::microseconds>(benchmarker.m_end - benchmarker.m_start).count();

        return out;
    }

    TimePoint getDuration() {
        return m_end - m_start;
    }

    TimePoint getStartTime() {
        return m_start;
    }

    TimePoint getEndTime() {
        return m_end;
    }

    bool isRunning() {
        return m_running;
    }
};

namespace {
    template<typename Int>
    constexpr bool isPrime(Int num, Int test = 2) {
        return (test * test > num ? true : (num % test == 0 ? false : isPrime(num, test + 1)));
    }
    //Buckets must be a size
    template<typename data_t, std::size_t PRIME_SIZE, typename = typename std::enable_if<isPrime(PRIME_SIZE)>::type>
    class BucketVector
    {
    public:
        constexpr static auto SIZE = PRIME_SIZE;
        template<bool is_const>
        using BucketIteratorBase = typename  std::iterator<std::bidirectional_iterator_tag, typename std::conditional<is_const, const data_t, data_t>::type>;
        using uint_t = std::uintptr_t;
        using BucketType = std::vector<data_t>;
        template<bool is_const>
        class BucketIterator : public BucketIteratorBase<is_const> {
        public:
            using Base = BucketIteratorBase<is_const>;
            using BucketOwner = BucketVector<data_t, PRIME_SIZE>;
            using typename Base::pointer;
            using typename Base::reference;
            using typename Base::value_type;
            friend class BucketIterator<!is_const>;
            std::size_t m_bucket;
            pointer m_value;
            BucketOwner* m_owner;
        public:
            BucketIterator(std::size_t bucket, pointer value, BucketOwner* owner)
                : m_bucket(bucket),
                m_value(value),
                m_owner(owner) {
                //validateIterator();
            }
            ~BucketIterator() {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator(const BucketIterator<value>& iterator)
                : m_bucket(iterator.m_bucket),
                m_value(iterator.m_value),
                m_owner(iterator.m_owner) {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator(BucketIterator<value>&& iterator)
                : m_bucket(std::move(iterator.m_bucket)),
                m_value(std::move(iterator.m_value)),
                m_owner(std::move(iterator.m_owner)) {
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator& operator=(BucketIterator<value>&& iterator) {
                m_bucket = std::move(iterator.m_bucket);
                m_value = std::move(iterator.m_value);
                m_owner = std::move(iterator.m_owner);
                return *this;
            }
            template<bool value, typename = typename std::enable_if<!value || (value == is_const)>::type>
            BucketIterator& operator=(const BucketIterator<value>& iterator) {
                m_bucket = iterator.m_bucket;
                m_value = iterator.m_value;
                m_owner = iterator.m_owner;
                return *this;
            }
            BucketIterator& operator++() {
                ++m_value;
                forwardValidate();
                return *this;
            }
            BucketIterator operator++(int) {
                BucketIterator copy(*this);
                ++(*this);
                return copy;
            }
            BucketIterator& operator--() {
                backwardValidate();
                --m_value;
                return *this;
            }
            BucketIterator operator--(int) {
                BucketIterator copy(*this);
                --(*this);
                return copy;
            }
            reference operator*() const {
                return *m_value;
            }
            pointer operator->() const {
                return m_value;
            }
            template<bool value>
            bool operator==(const BucketIterator<value>& iterator) const {
                return m_bucket == iterator.m_bucket && m_owner == iterator.m_owner && m_value == iterator.m_value;
            }
            template<bool value>
            bool operator!=(const BucketIterator<value>& iterator) const {
                return !(this->operator==(iterator));
            }
            BucketOwner* getSystem() const {
                return m_owner;
            }
            inline void backwardValidate() {
                while (m_value == m_owner->m_buckets[m_bucket].data() && m_bucket != 0) {
                    --m_bucket;
                    m_value = m_owner->m_buckets[m_bucket].data() + m_owner->m_buckets[m_bucket].size();
                }
            }
            inline void forwardValidate() {
                while (m_value == (m_owner->m_buckets[m_bucket].data() + m_owner->m_buckets[m_bucket].size()) && m_bucket != SIZE - 1) {
                    m_value = m_owner->m_buckets[++m_bucket].data();
                }
            }
        };
        using iterator = BucketIterator<false>;
        using const_iterator = BucketIterator<true>;
        friend class BucketIterator<false>;
        friend class BucketIterator<true>;
    private:
        std::array<BucketType, SIZE> m_buckets;
        std::size_t m_size;
    public:
        BucketVector()
            : m_size(0) {
        }
        ~BucketVector() {
        }
        BucketVector(const BucketVector&) = default;
        BucketVector(BucketVector&&) = default;
        BucketVector& operator=(const BucketVector&) = default;
        BucketVector& operator=(BucketVector&&) = default;
        data_t& operator[](std::size_t index) {
            const auto bucketIndex = findBucketIndex(index);
            return m_buckets[bucketIndex.first][bucketIndex.second];
        }
        const data_t& operator[](std::size_t index) const {
            return static_cast<BucketVector*>(this)->operator[](index);
        }
        data_t& at(std::size_t index) {
            if (index >= m_size) {
                throw std::out_of_range("BucketVector::at index out of range");
            }
            return this->operator[](index);
        }
        const data_t& at(std::size_t index) const {
            return static_cast<BucketVector*>(this)->at(index);
        }
        void erase(const_iterator iter) {
            auto& bucket = m_buckets[iter.m_bucket];
            std::size_t index = iter.m_value - bucket.data();
            bucket[index] = bucket.back();
            bucket.pop_back();
            --m_size;
        }
        void push_back(uint_t id, const data_t& data) {
            const auto slot = get_slot(id);
            m_buckets[slot].push_back(data);
            ++m_size;
        }
        void push_back(uint_t id, data_t&& data) {
            const auto slot = get_slot(id);
            m_buckets[slot].push_back(std::move(data));
            ++m_size;
        }
        template<typename... args>
        void emplace_back(uint_t id, args&&... parameters) {
            const auto slot = get_slot(id);
            m_buckets[slot].emplace_back(std::forward<args>(parameters)...);
            ++m_size;
        }

        void pop_back(uint_t index) {
            const auto slot = get_slot(index);
            m_buckets[slot].pop_back();
            --m_size;
        }
        void pop_front(uint_t index) {
            const auto slot = get_slot(index);
            m_buckets[slot].pop_front();
            --m_size;
        }
        void reserve(std::size_t size) {
            const std::size_t slotSize = size / SIZE + 1;
            for (auto& bucket : m_buckets) {
                bucket.reserve(slotSize);
            }
        }
        void clear() {
            for (auto& bucket : m_buckets) {
                bucket.clear();
            }
        }
        bool empty() const {
            return m_size != 0;
        }
        std::size_t size() const {
            return m_size;
        }
        iterator find(uint_t index, const data_t& value) {
            const std::size_t slot = get_slot(index);
            auto& bucket = m_buckets[slot];
            for (auto it = bucket.begin(), end = bucket.end(); it != end; ++it) {
                if (*it == value) {
                    return { slot, &(*it), this };
                }
            }
            return end();
        }
        template<typename fn_t>
        iterator find(uint_t index, const fn_t& fn) {
            const std::size_t slot = get_slot(index);
            auto& bucket = m_buckets[slot];
            for (auto it = bucket.begin(), end = bucket.end(); it != end; ++it) {
                if (fn(*it)) {
                    return { slot, &(*it), this };
                }
            }
            return end();
        }
        const_iterator find(uint_t index, const data_t& value) const {
            return cfind(index, value);
        }
        const_iterator cfind(uint_t index, const data_t& value) const {
            return static_cast<BucketVector*>(this)->find(index, value);
        }
        iterator begin(uint_t index = 0) {
            auto bucketIndex = findBucketIndex(index);
            iterator it{ bucketIndex.first, m_buckets[bucketIndex.first].data() + bucketIndex.second, this };
            it.forwardValidate();
            return it;
        }
        iterator end(uint_t index = 0) {
            iterator it{ SIZE - 1, m_buckets.back().data() + m_buckets.back().size(), this };
            return it;
        }
        const_iterator begin(uint_t index = 0) const {
            auto bucketIndex = findBucketIndex(index);
            const_iterator it{ bucketIndex.first, m_buckets[bucketIndex.first].data() + bucketIndex.second, this };
            it.forwardValidate();
            return it;
        }
        const_iterator end(uint_t index = 0) const {
            const_iterator it{ SIZE - 1, m_buckets.back().data() + m_buckets.back().size(), this };
            return it;
        }
        std::size_t get_slot(uint_t id) {
            return id % SIZE;
        }
    private:
        inline std::pair<std::size_t, std::size_t> findBucketIndex(std::size_t index) {
            std::size_t bucket = 0;
            std::size_t count = 0;
            while (index >= m_buckets[bucket].size() + count) {
                count += m_buckets[bucket].size();
                ++bucket;
            }
            return { bucket, index - count };
        }
    };
}

constexpr std::size_t SIZE = 1'000;
constexpr std::size_t INDEXES = 400;
constexpr std::size_t SPACING = 26;

void vectorFindErase(std::vector<int>& values, int value) {
    const auto end = values.end();
    for (auto it = values.begin(); it != end; ++it) {
        if (*it == value) {
            values.erase(it);
            break;
        }
    }
}
void vectorEraseSorted(std::vector<int>& values, int value) {
    auto it = std::lower_bound(values.begin(), values.end(), value);
    if (it != values.end() && !(value < *it)) {
        values.erase(it);
    }
}

void setErase(std::unordered_set<int>& values, int value) {
    values.erase(value);
}
int main() {
    std::mt19937 rng;
    rng.seed(std::random_device()());


    std::vector<int> values(SIZE);
    std::generate_n(values.begin(), SIZE, []() {
        static int index = 0;
        return index++;
    });
    auto sorted = values;
    auto preallocate = values;
    auto vnf = values;

    std::random_shuffle(vnf.begin(), vnf.end(), [&](auto i) {
        return rng() % i;
    });
    std::vector<int> indexes(INDEXES);
    std::generate(indexes.begin(), indexes.end(), [&]() {
        return rng() % SIZE;
    });

    //APPEND VALUES TO BUCKET VECTOR, USE VALUE AS IT'S OWN KEY
    BucketVector<int, 23> bucket;
    for (auto& value : values) {
        bucket.push_back(value, value);
    }



    Benchmarker<> bench(true);

    //NAIVE FIND AND ERASE
    for (auto& index : indexes) {
        vectorFindErase(vnf, index);
    }
    std::cout << std::left;
    std::cout << std::setw(SPACING) << "Naive Find and Erase: " << bench.stop() << '\n';

    //SORTED ERASE
    bench.start();
    for (auto& index : indexes) {
        vectorEraseSorted(sorted, index);
    }
    std::cout << std::setw(SPACING) << "Sorted erase: " << bench.stop() << '\n';

    //PRELLOCATED ERASE
    bench.start();
    for (auto& index : indexes) {
        preallocate[index] = std::numeric_limits<int>::min();
    }
    std::cout << std::setw(SPACING) << "Prellocated erase: " << bench.stop() << '\n';

    //BUCKETVECTOR ERASE
    bench.start();
    for (auto& index : indexes) {
        auto it = bucket.find(index, index);
        if (it == bucket.end()) {
            continue;
        }
        bucket.erase(it);
    }

    std::cout << std::setw(SPACING) << "BucketVector erase: " << bench.stop() << '\n';

    //BUCKET SUM/ITERATE
    bench.start();
    long long bucketSum = 0;
    for (std::size_t index = 0; index != 10'000; ++index) {
        for (auto& val : bucket) {
            bucketSum += val;
        }
    }
    std::cout << std::setw(SPACING) << "Bucket Sum/Iterate: " << bench.stop() << ' ' << bucketSum << '\n';


    //PREALLOCATE SUM/ITERATE
    bench.start();
    long long vfsum = 0;
    for (std::size_t index = 0; index != 10'000; ++index) {
        for (auto& val : preallocate) {
            if (val != std::numeric_limits<int>::min()) {
                vfsum += val;
            }
        }
    }

    std::cout << std::setw(SPACING) << "Preallocate sum/Iterate: " << bench.stop() << ' ' << vfsum << '\n';
    std::cin.get();

    return 0;
}

На моем компьютере яобнаружил, что BucketVector выполнялся несколько быстрее, чем предварительно выделенный массив, когда предварительно выделенный массив был на 50% или более пустым с размером 1000.

...