Самый быстрый контейнер или алгоритм для уникальных идентификаторов повторного использования в С++

У меня есть потребность в уникальных идентификаторах многократного использования. Пользователь может выбрать свои собственные идентификаторы, или он может попросить бесплатный. API в основном

class IdManager {
public:
  int AllocateId();          // Allocates an id
  void FreeId(int id);       // Frees an id so it can be used again
  bool MarkAsUsed(int id);   // Let the user register an id. 
                             // returns false if the id was already used.
  bool IsUsed(int id);       // Returns true if id is used.
};

Предположим, что идентификаторы начинаются с 1 и прогресс, 2, 3 и т.д. Это не требование, просто чтобы проиллюстрировать.

IdManager mgr;
mgr.MarkAsUsed(3);
printf ("%d\n", mgr.AllocateId());
printf ("%d\n", mgr.AllocateId());
printf ("%d\n", mgr.AllocateId());

Выведет

1
2
4

Потому что id 3 уже объявлен используемым.

Какой лучший контейнер/алгоритм для обоих помнят, какие идентификаторы используются И найти бесплатный id?

Если вы хотите узнать конкретный вариант использования, OpenGL glGenTextures, glBindTexture и glDeleteTextures эквивалентны AllocateId, MarkAsUsed и FreeId

Ответ 1

Моя идея - использовать std::set и Boost.interval, поэтому IdManager будет содержать набор неперекрывающихся интервалов свободных идентификаторов. AllocateId() очень просто и очень быстро и просто возвращает левую границу первого свободного интервала. Другие два метода несколько сложнее, потому что может потребоваться разделение существующего интервала или слияние двух соседних интервалов. Однако они также довольно быстро.

Итак, это иллюстрация идеи использования интервалов:

IdManager mgr;    // Now there is one interval of free IDs:  [1..MAX_INT]
mgr.MarkAsUsed(3);// Now there are two interval of free IDs: [1..2], [4..MAX_INT]
mgr.AllocateId(); // two intervals:                          [2..2], [4..MAX_INT]
mgr.AllocateId(); // Now there is one interval:              [4..MAX_INT]
mgr.AllocateId(); // Now there is one interval:              [5..MAX_INT]

Это сам код:

#include <boost/numeric/interval.hpp>
#include <limits>
#include <set>
#include <iostream>


class id_interval 
{
public:
    id_interval(int ll, int uu) : value_(ll,uu)  {}
    bool operator < (const id_interval& ) const;
    int left() const { return value_.lower(); }
    int right() const {  return value_.upper(); }
private:
    boost::numeric::interval<int> value_;
};

class IdManager {
public:
    IdManager();
    int AllocateId();          // Allocates an id
    void FreeId(int id);       // Frees an id so it can be used again
    bool MarkAsUsed(int id);   // Let the user register an id. 
private: 
    typedef std::set<id_interval> id_intervals_t;
    id_intervals_t free_;
};

IdManager::IdManager()
{
    free_.insert(id_interval(1, std::numeric_limits<int>::max()));
}

int IdManager::AllocateId()
{
    id_interval first = *(free_.begin());
    int free_id = first.left();
    free_.erase(free_.begin());
    if (first.left() + 1 <= first.right()) {
        free_.insert(id_interval(first.left() + 1 , first.right()));
    }
    return free_id;
}

bool IdManager::MarkAsUsed(int id)
{
    id_intervals_t::iterator it = free_.find(id_interval(id,id));
    if (it == free_.end()) {
        return false;
    } else {
        id_interval free_interval = *(it);
        free_.erase (it);
        if (free_interval.left() < id) {
            free_.insert(id_interval(free_interval.left(), id-1));
        }
        if (id +1 <= free_interval.right() ) {
            free_.insert(id_interval(id+1, free_interval.right()));
        }
        return true;
    }
}

void IdManager::FreeId(int id)
{
    id_intervals_t::iterator it = free_.find(id_interval(id,id));
    if (it != free_.end()  && it->left() <= id && it->right() > id) {
        return ;
    }
    it = free_.upper_bound(id_interval(id,id));
    if (it == free_.end()) {
        return ;
    } else {
        id_interval free_interval = *(it);

        if (id + 1 != free_interval.left()) {
            free_.insert(id_interval(id, id));
        } else {
            if (it != free_.begin()) {
                id_intervals_t::iterator it_2 = it;
                --it_2;
                if (it_2->right() + 1 == id ) {
                    id_interval free_interval_2 = *(it_2);
                    free_.erase(it);
                    free_.erase(it_2);
                    free_.insert(
                        id_interval(free_interval_2.left(), 
                                    free_interval.right()));
                } else {
                    free_.erase(it);
                    free_.insert(id_interval(id, free_interval.right()));
                }
            } else {
                    free_.erase(it);
                    free_.insert(id_interval(id, free_interval.right()));
            }
        }
    }
}

bool id_interval::operator < (const id_interval& s) const
{
    return 
      (value_.lower() < s.value_.lower()) && 
      (value_.upper() < s.value_.lower());
}


int main()
{
    IdManager mgr;

    mgr.MarkAsUsed(3);
    printf ("%d\n", mgr.AllocateId());
    printf ("%d\n", mgr.AllocateId());
    printf ("%d\n", mgr.AllocateId());

    return 0;
}

Ответ 2

Как правило, я бы сказал, придерживаться простой реализации, пока вы не знаете, какие методы используются больше всего. Преждевременная настройка может оказаться неправильной. Используйте простую реализацию и зарегистрируйте ее использование, затем вы можете оптимизировать функции, которые используются больше всего. Не используйте оптимизацию для быстрого удаления или быстрого выделения, если вам потребуется только несколько сотен идентификаторов, и простого вектора будет достаточно.

Ответ 3

Но я не думаю, что вы должны гарантировать, что идентификатор должен начинаться с 1. Вы можете просто убедиться, что доступный идентификатор должен быть больше, чем все выделенные идентификаторы.

Как и при первом регистрации 3, следующий доступный идентификатор может быть равен 4. Я не думаю, что нужно использовать 1.

Ответ 4

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

Лично я бы пошел на следующее:

  • set, что помогает легко отслеживать идентификаторы O(log N)
  • предлагая новый идентификатор как текущий максимум + 1... O(1)

Если вы не выделяете (в течение жизни приложения) больше, чем max<int>() ids, это должно быть хорошо, иначе... используйте более крупный тип (сделайте его неподписанным, используйте long или long long), что проще всего начать с.

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

Ответ 5

Я предполагаю, что вы хотите использовать все доступные значения для типа Id и хотите повторно использовать освобожденные идентификаторы? Я также предполагаю, что вы заблокируете коллекцию, если используете ее из нескольких потоков...

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

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

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

Чтобы зарезервировать идентификатор, вы просто проверяете набор, а если нет, добавьте его.

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

Ответ 6

Сжатый вектор. Но я не думаю, что какой-либо контейнер имел бы заметную разницу.

Ответ 7

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

IdManager::IdManager()
{
    m_map.insert(std::make_pair(std::numeric_limits<int>::max(), 1);
}

int IdManager::AllocateId()
{
    assert(!m_map.empty());
    MyMap::iterator p = m_map.begin();
    int id = p->second;
    ++p->second;
    if (p->second > p->first)
        m_map.erase(p);
    return id;
}

void IdManager::FreeId(int id)
{
    // I'll fill this in later
}

bool IdManager::MarkAsUsed(int id)
{
    MyMap::iterator p = m_map.lower_bound(id);

    // return false if the ID is already allocated
    if (p == m_map.end() || id < p->second || id > p->first)))
        return false;

    // first thunderstorm of the season, I'll leave this for now before the power glitches
}

bool IdManager::IsUsed(int id)
{
    MyMap::iterator p = m_map.lower_bound(id);
    return (p != m_map.end() && id >= p->second && id <= p->first);
}

Ответ 8

Итак, друг указал, что в этом случае хэш может быть лучше. Большинство программ OpenGL не используют более нескольких тысяч идентификаторов, поэтому хэш с 4096 слотами почти гарантированно имеет только 1 или 2 записи на каждый слот. Существует некоторый дегенеративный случай, когда множество идентификаторов может идти в 1 слот, но это серьезно маловероятно. Использование хэша сделает AllocateID намного медленнее, но для этого может быть использован набор. Выделение более медленным является менее важным, чем InUse, быстро используемым для моего использования.