C ++ STL - реализация контейнеров - PullRequest
0 голосов
/ 22 января 2020

В настоящее время я много узнаю о навязчивых контейнерах. Поэтому я часто сравниваю их со стандартными контейнерами.

Давайте возьмем для примера std :: list. Я читал, что этот контейнер обычно реализован в виде двусвязного списка. Но я не читал ничего о том, как узлы реализованы в деталях. Я предполагаю, что есть указатель «предыдущий» и «следующий», но как насчет объекта, который принадлежит такому узлу. Это указатель на объект или сам объект, который создается в узлах, выделенных памяти?

В Boost.Intrusive указано, что их контейнеры имеют лучшую локальность (см. Здесь: https://www.boost.org/doc/libs/1_72_0/doc/html/intrusive/usage_when.html, или здесь: https://www.boost.org/doc/libs/1_72_0/doc/html/intrusive/performance.html). Я не уверен, почему это так. Когда узел в std :: list содержит объект, а навязчивый контейнер содержит узел в своем объекте, как это приводит к лучшей локализации?

1 Ответ

1 голос
/ 23 января 2020

Указатель на объект или сам объект, который создается в выделенной памяти узлов?

В Boost.Intrusive, тип элемента контейнера это узел. Чтобы сделать его совместимым с контейнером, необходимо изменить тип элемента так, чтобы он включал элементы данных, требуемые контейнером, - либо путем наследования от базового класса (например, list_base_hook, см. здесь ) или добавив специальный элемент данных (например, list_member_hook). Вот почему они называются навязчивыми контейнерами. Для сравнения, стандартные контейнеры не требуют, чтобы вы изменяли ваши классы и вместо этого помещали их в узлы контейнера, если это необходимо.

Когда узел в списке std :: list содержит объект и навязчивый контейнер удерживает узел в своем объекте, как это приводит к лучшей локализации?

В std::list каждый контейнерный узел (который содержит ваш элемент) выделяется отдельно, в своем собственном динамическом c выделении памяти , Узел содержит указатели на предыдущий и следующий элементы в списке. Поскольку каждый узел выделяется отдельно, их месторасположение зависит от используемого распределителя памяти, но, как правило, можно предположить, что разные узлы расположены в произвольных местах в памяти, возможно, далеко и не по порядку. Кроме того, обход списка требует разыменования указателя на следующий элемент на каждой итерации, что обычно затрудняет алгоритмы кэширования памяти в ЦП.

В boost::intrusive::list контейнер не налагает какую-либо стратегию выделения памяти для пользователь. Можно иметь одну область памяти для нескольких или всех элементов контейнера для навязывания, что делает их более плотно упакованными и, возможно, упорядоченными в памяти. Это требует больше работы от пользователя, конечно. Итерация списка все еще требует разыменования указателя и вредит предварительному извлечению в ЦП, но если элементы контейнера тесно упакованы, велика вероятность того, что каждый следующий узел будет находиться в строке кэша, которая уже была извлечена из памяти для предыдущего элемента (элементов).

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

// Element type
class Foo {};

std::list< std::shared_ptr< Foo > > list;
std::map< int, std::shared_ptr< Foo > > map;

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

С помощью интрузивных контейнеров вы можете уменьшить это до одного выделения без дополнительной косвенности:

// List hook
typedef boost::intrusive::list_base_hook<> FooListHook;
// Map/set hook
typedef boost::intrusive::set_base_hook<
    boost::intrusive::optimize_size< true >
> FooSetHook;

// Node type
class Foo :
    public FooListHook,
    public FooSetHook
{
    ...
};

boost::intrusive::list< Foo, boost::intrusive::base_hook< FooListHook > > list;
boost::intrusive::set< Foo, boost::intrusive::base_hook< FooSetHook >, ... > set;

В этом случае ни list, ни set не занимаются выделением памяти самостоятельно, все необходимые данные уже находятся в узле Foo, который вы выделяете сами. Итерация через любой из контейнеров автоматически извлекает оба хука и содержимое Foo (по крайней мере, частично) в кеш, что ускоряет доступ к памяти. У этого подхода есть и другие преимущества, такие как возможность переключения между итераторами двух контейнеров без дорогостоящего поиска элементов.

...