очистка nullptr в отношении один-ко-многим, которые используют настраиваемый слабый указатель

У меня есть класс карты «один ко многим» - 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) получить O(1) и гарантировать, что каждый слот Room::bodys занят.

введите здесь описание изображения

Вопрос

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

введите здесь описание изображения

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

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

  • В моем случае, к счастью, стоимость проверки того, «является ли WeakPtr<> nullptr», очень дешевая.
  • У каждого экземпляра есть свой уникальный int ID.
    Идентификатор выполняется раздельно для каждого типа, а значение идентификатора низкое (потому что я его перерабатываю).
  • Я использую многопоточность.
  • 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.
Это быстрее. Возможно, это потому, что компьютер любит пакетирование. (например, меньшая стоимость vtable). Тем не менее, он по-прежнему использует процессор примерно на 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-й временной шаг.

введите здесь описание изображения

Решение 4

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

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

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


person javaLover    schedule 29.04.2019    source источник
comment
Может ли быть вариант изменить метод сложения, чтобы попытаться использовать пустой слот перед добавлением в конец массива? Кстати, вы называете это массивом, но это должен быть вектор ...   -  person Serge Ballesta    schedule 29.04.2019
comment
@Serge Ballesta: Да, это возможно, но мне нужно искать это с помощью O(array-length). В настоящее время это просто O(1). Кстати, я думаю, что vector означает std::vector, а array - более общий термин, поэтому array - более подходящее слово, не так ли?   -  person javaLover    schedule 29.04.2019
comment
Я вижу, у вас есть метод getAllChildren. Вам нужно часто перебирать всех дочерних элементов? Если да - провести чистку во время итерации довольно просто.   -  person Dmitry Gordon    schedule 29.04.2019
comment
Нет проблем с термином array. Я просто заметил, что в C ++ очень часто инкапсулируют массивы в std::vector. По большей части, если я правильно помню, многие инструменты, работающие со слотами, могут стать пустыми, часто используют связанный список пустых слотов для ускорения умного выделения. Но цель в другом: избежать сжатия массива array каждый раз, когда что-то удаляется.   -  person Serge Ballesta    schedule 29.04.2019
comment
@ Дмитрий Гордон Ха-ха, я понимаю, о чем вы. В большинстве случаев MyMap1N - да, но нет никаких гарантий. В некоторых случаях я обычно повторяю до 10 раз за один временной шаг (может привести к слишком частой очистке). Для других случаев я обычно повторяю один раз каждые 3 временных шага (может привести к чрезмерному размеру массива). Кстати, полезная идея, спасибо.   -  person javaLover    schedule 29.04.2019
comment
Может быть, вы можете ввести какой-нибудь счетчик в родителя. Каждый раз, когда один из дочерних объектов уничтожается и когда значение счетчика превышает какое-то значение - вы можете выполнить чистую   -  person Dmitry Gordon    schedule 29.04.2019
comment
@Dmitry Gordon Я могу только представить, что это должно быть реализовано с помощью шаблона обратного вызова, аналогичного Решению 1. Я не думаю, что это будет быстро. Возможно, я смогу применить вашу идею на глобальном уровне - подсчитать удаление RigidBody - а затем провести глобальную очистку для любого связанного экземпляра MyMap1N. Возможно, мне придется замедлить его, используя mutex, потому что я также использую многопоточность (вопрос отредактирован). Или я могу проигнорировать состояние гонки, потому что приблизительного значения достаточно. Интересная идея, благодарю.   -  person javaLover    schedule 29.04.2019
comment
Почему обратный звонок? Вы можете сохранить слабую ссылку на родительский элемент и просто увеличивать атомарное значение в деструкторе. Вам нужно будет удерживать мьютекс только при очистке, это относительно редкий случай.   -  person Dmitry Gordon    schedule 29.04.2019
comment
@Dmitry Gordon У меня много MyMap1N, относящихся к какому-то определенному типу (например, RigidBody). Если RigidBody удален, почему каждый MyMap1N знает об удалении? Я должен заставить rigidBodyX::destroy(){ позвонить relatedMap1->notifyDelete(rigidBodyX), relatedMap2->notifyDelete(rigidBodyX), ... }. Если я не хочу жестко кодировать, я не могу найти других способов, кроме регистрации всех связанных MyMap1N для прослушивания при удалении rigidBodyX. В настоящее время я регистрирую обратный вызов, используя тип в шаблоне, т.е. <WeakPtr_Parent,WeakPtr_Children> автоматически.   -  person javaLover    schedule 29.04.2019
comment
@ Дмитрий Гордон Это может сбивать с толку. Если я не объясню достаточно ясно, спросите.   -  person javaLover    schedule 29.04.2019
comment
Да, я понимаю - один дочерний объект может встречаться на нескольких картах. Но в любом случае - вы храните индекс в родительском массиве, поэтому для каждой карты вы инициализируете индекс в родительском массиве. Таким образом, вы можете иметь вектор ссылок на родительскую карту и инициализировать его там, где вы инициализируете indexInArray. Извините, если я что-то не так понял. Возможно, простой пример, когда все поля Room и RigidBody удалены, поможет в описании вопроса.   -  person Dmitry Gordon    schedule 29.04.2019
comment
@ Дмитрий Гордон Нет, ты все правильно понял. XD. Я тоже могу сохранить ссылку на родительскую карту. То, что вы заявили, также является возможным решением (я пробовал это 2 года назад - но это очень медленно - это меня очень огорчило). К сожалению, исходя из моих ощущений после долгого профилирования, любая попытка ссылаться {от дочернего - на родительскую карту}, будь то на сайте удаления или пакетно в конце временного шага, требует значительных затрат процессора. ... Я все еще размышляю о том, как считать, что вы заявили ... с некоторыми изменениями, это может быть лучший способ.   -  person javaLover    schedule 29.04.2019
comment
номера идентификаторов? Возможно, вы могли бы просто использовать идентификатор в качестве индекса - вы сказали, что у вас уже работает переработка идентификаторов. Новые элементы заменят удаленные элементы, а размер массива будет более или менее постоянным. Кстати, это C ++ в лучшем виде - это не программирование, это управление памятью :)   -  person Krzysztof Skowronek    schedule 29.04.2019
comment
@Krzysztof Skowronek Да, это int (я отредактирую вопрос). Да, используя только метод ID, я могу гарантировать, что длина Room::bodys всегда меньше определенного числа, например. 1000, что еще далеко не эффективно. :)   -  person javaLover    schedule 29.04.2019
comment
@javaLover, вы, ребята, c ++, странно относитесь к эффективности - если идентификаторы доходят до 1000, это означает, что в какой-то момент у вас будет 1000 RigidBodies. Вы делаете игру, поэтому я думаю, что вы можете оценить максимальное количество (или что-то близкое к максимальному) для каждого уровня, чтобы заранее выделить массив. Наличие 1000 пустых указателей не так уж и плохо, если это ускоряет игру. Если вы не стремитесь играть в эту игру на Arduino, я бы сказал, что это достаточно хорошо. Таким образом вы, по сути, заменяете карту массивом   -  person Krzysztof Skowronek    schedule 29.04.2019
comment
Другая идея - иметь на вашей карте набор (unordered_set), содержащий бесплатные индексы. Вернитесь к первому решению, но вместо удаления указателя и переупаковки просто добавьте индекс в набор. При добавлении элемента в карту / массив возьмите индекс из набора (или даже сделайте его стеком и вытяните его) и установите значение указателя   -  person Krzysztof Skowronek    schedule 29.04.2019
comment
@Krzysztof Skowronek Мой пример плохой. Более разумный пример - 1000 родителей и 1000 детей. Используя резервирование в стиле MAX, мне придется зарезервировать для карты 1000 массивов с размером = 1000. Это неэффективно. XD .... Интересно твое сет / стек-решение. Если я правильно понимаю, мне нужно добавить 1 стек на каждый экземпляр родителя, верно? Я никогда не думал об этом. Благодарить.   -  person javaLover    schedule 29.04.2019
comment
да, по одному стеку на каждого родителя. Что касается эффективности, это 16 байт на weak_ptr - я думал, weak_ptr намного меньше. Ссылка .NET занимает 8 байт на платформах x64 и может намного больше. Тем не менее, это 15 МБ для 1000 родителей с 1000 детей - неплохо, учитывая, что на компьютере 8 ГБ оперативной памяти. Эффективность памяти очень относительна. Конечно, если вы зададите 5 таких тем, это сложится, но одна? Пожалуйста: P   -  person Krzysztof Skowronek    schedule 29.04.2019
comment
@Krzysztof Skowronek Да, 15 МБ - это не так много в наши дни. Я беспокоюсь о getAllChildren(room). При итерации 1000 (возможно, пустых) дочерних слотов будет много пропусков кеша. :П   -  person javaLover    schedule 29.04.2019
comment
Вы сказали, что проверка стоит недорого. В любом случае, удачи :)   -  person Krzysztof Skowronek    schedule 29.04.2019
comment
Как насчет использования специального средства удаления для вашего экземпляра RigidBody, которое позаботится об удалении ссылок из контейнеров (например, карты)? См. Мой ответ на другой пост: контейнер C ++, который автоматически удаляет элементы при разрушении   -  person Lorenz Zhao    schedule 29.04.2019
comment
@Lorenz Zhao Я думаю, что у него тот же недостаток, что и у решения 1.   -  person javaLover    schedule 29.04.2019
comment
@Krzysztof Skowronek Да, это дешево, но даже сравнение 1000 экземпляров дешевых вещей (таких как необработанные указатели == nullptr) не так уж и весело для процессора и памяти, когда этого можно избежать. ИМХО, Решение 2 намного дешевле. Хотя я еще не тестировал.   -  person javaLover    schedule 29.04.2019


Ответы (1)


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

Решение 1

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

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

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

Недостатки

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

Решение 2

Есть еще одно решение аналогичного типа, о котором шла речь в комментариях. Однако вместо RigidBody, имеющего несколько индексов для каждого родителя, у него есть один уникальный идентификатор, который действует как индекс. Этот уникальный идентификатор должен иметь известный диапазон минимальных и максимальных значений. Затем каждый родитель выделит достаточно места для размещения максимального количества идентификаторов и жестких тел. Уничтожение и удаление 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%.

Базовая программа тестирования

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

#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.

person Rabster    schedule 01.05.2019
comment
Кстати, мне нравится усовершенствование вашего решения 3 в моем решении 4. Спасибо +1 - person javaLover; 01.05.2019
comment
Просто чтобы проверить мое понимание; ваше Решение 1 имеет тот же недостаток, что и мое Решение 1-2, а ваше Решение 4 имеет тот же недостаток, что и std::unordered_set (но с более дешевой хэш-функцией). Верный? - person javaLover; 01.05.2019
comment
Да, вы правы, что оно имеет тот же недостаток, что и ваше Решение 1-2. Однако удаление RigidBody из единственного родителя обходится дешево с такой формой структуры данных. Вопрос, сколько разных родителей может быть у RigidBody? Четвертое решение по существу включает в себя нишевую структуру, представляющую собой гибрид производительности между std::unordered_set и std::vector, который может быть полезен. - person Rabster; 01.05.2019