Reputation: 343
I am currently learning a lot about Intrusive Containers. So I often compare them to the standard containers.
Let's take the std::list for example. I read that this container is usually implemented as a doubly-linked list. But I didn't read anything of how the nodes are implemented in detail. I assume there is a 'previous' and 'next' pointer, but what about the object that belongs to such a node. Is it a Pointer to an object, or the object itself, that is constructed in the nodes allocated memory?
In Boost.Intrusive it is stated that their containers have a better locality (see here: https://www.boost.org/doc/libs/1_72_0/doc/html/intrusive/usage_when.html, or here: https://www.boost.org/doc/libs/1_72_0/doc/html/intrusive/performance.html). I am not sure why that is the case. When the node in the std::list holds an object and the intrusive container holds a node in their object, how does that lead to better locality?
Upvotes: 0
Views: 420
Reputation: 10606
Is it a Pointer to an object, or the object itself, that is constructed in the nodes allocated memory?
In Boost.Intrusive, the element type of the container is the node. In order to make it compatible with the container, you have to modify your element type so that it includes data members required by the container - either by inheriting from a base class (e.g. list_base_hook
, see here) or by adding a special data member (e.g. list_member_hook
). This is why they are called intrusive containers. In comparison, the standard containers do not require you to modify your classes and instead wrap them in container nodes, if necessary.
When the node in the std::list holds an object and the intrusive container holds a node in their object, how does that lead to better locality?
In std::list
each container node (which contains your element) is allocated separately, in its own dynamic memory allocation. The node contains pointers to the previous and the next elements in the list. Since each node is allocated separately, their locality depends on the memory allocator used, but generally you can assume that different nodes are located at arbitrary locations in memory, possibly distant and not in order. Additionally, traversing the list requires to dereference a pointer to the next element on each iteration, which typically hampers memory caching algorithms in the CPU.
In boost::intrusive::list
, the container does not impose any memory allocation strategy on the user. It is possible to have a single memory region for multiple or all elements of the intrusive container, which makes them more closely packed and possibly ordered in memory. This requires more work from the user, of course. List iteration still requires pointer dereferencing and hurts prefetcher in the CPU, but if container elements are closely packed, chances are high that each next node will be in a cache line that was already fetched from memory for the previous element(s).
Another thing to note is that intrusive containers are much more useful when you need to store element in multiple containers at once. With standard containers, you have to use pointers to refer to the element from each container. For example:
// Element type
class Foo {};
std::list< std::shared_ptr< Foo > > list;
std::map< int, std::shared_ptr< Foo > > map;
In this example, you have at least one allocation for a Foo
object, one allocation for the list
node and one allocation for the map
node. Each of these allocations is arbitrarily located in memory. Accessing the element either via list
or via map
requires an additional level of indirection.
With intrusive containers, you can reduce this to just one allocation and no extra indirection:
// 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;
In this case, neither list
nor set
does memory allocation of their own, all the necessary data is already within the Foo
node, which you allocate yourself. Iterating through either of the containers automatically fetches both hooks and Foo
contents (at least partially) into cache, which makes memory accesses faster. There are other benefits to this approach as well, like the ability to switch between the two containers' iterators without expensive element lookup.
Upvotes: 1