C++ STL - 容器实现

Posted

技术标签:

【中文标题】C++ STL - 容器实现【英文标题】:C++ STL - Containers implementation 【发布时间】:2020-01-22 19:01:54 【问题描述】:

我目前正在学习很多关于侵入式容器的知识。所以我经常将它们与标准容器进行比较。

我们以 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 中的节点持有一个对象,而侵入式容器在其对象中持有一个节点时,这如何导致更好的局部性?

【问题讨论】:

没有关于如何实现标准容器的详细信息,因为这取决于每个标准库的实现方式,只要它们尊重标准强加的其他要求。您可以查看任何给定的标准库,看看 那个 库是如何做到的,但没有通用的解决方案。 该标准没有指定如何实现容器,而是指定了行为和 API。例如,容器必须有方法 beginsize 将是 API 规范。他们也可能会说搜索必须至少为 O(logN) 或更快,这会提示实现,但不会特别限制任何内容。 列表的本地化通常很糟糕。提供更好位置的列表实现不会给我留下深刻印象。我想您可以通过简单地提供自己的分配器来为您的特定应用程序优化局部性,从而大大改善局部性。 @CoryKramer 请注意,list::splice 与 AllocatorAwareContainer 要求相结合,构成了非常精细的限制框架。对于其他容器,由于 c++17 node handles 对实现添加了类似的限制(实际上,它们基本上暴露了基于实际现有实现的文档接口中的优化潜力) @sehe map, set et.al.必须是某种平衡树,但确切地仍然取决于实现。 【参考方案1】:

它是指向对象的指针,还是对象本身,是在节点分配的内存中构造的?

在Boost.Intrusive中,容器的元素类型节点。为了使其与容器兼容,您必须修改元素类型,使其包含容器所需的数据成员 - 通过从基类继承(例如 list_base_hook,参见 here)或添加特殊数据成员(例如list_member_hook)。这就是为什么它们被称为 intrusive 容器。相比之下,标准容器不需要您修改类,而是在必要时将它们包装在容器节点中。

当 std::list 中的节点持有一个对象,而侵入式容器在其对象中持有一个节点时,这如何导致更好的局部性?

std::list 中,每个容器节点(包含您的元素)在其自己的动态内存分配中单独分配。该节点包含指向列表中前一个和下一个元素的指针。由于每个节点都是单独分配的,因此它们的位置取决于所使用的内存分配器,但通常您可以假设不同的节点位于内存中的任意位置,可能距离较远且不按顺序排列。此外,遍历列表需要在每次迭代时取消引用指向下一个元素的指针,这通常会妨碍 CPU 中的内存缓存算法。

boost::intrusive::list 中,容器不会对用户强加任何内存分配策略。可以为侵入式容器的多个或所有元素拥有一个内存区域,这使得它们在内存中更紧密地打包并可能有序。当然,这需要用户做更多的工作。列表迭代仍然需要指针解引用并损害 CPU 中的预取器,但如果容器元素紧密排列,则每个下一个节点很可能位于已从内存中为前一个元素获取的缓存行中。

另外需要注意的是,当您需要一次将元素存储在多个容器中时,侵入式容器会更加有用。对于标准容器,您必须使用指针来引用每个容器中的元素。例如:

// Element type
class Foo ;

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

在此示例中,您至少有一个 Foo 对象分配、一个 list 节点分配和一个 map 节点分配。这些分配中的每一个都任意位于内存中。通过listmap 访问元素需要额外的间接级别。

使用侵入式容器,您可以将其减少为仅一次分配,而无需额外的间接:

// 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;

在这种情况下,listset 都不会自己分配内存,所有必要的数据都已经在您自己分配的 Foo 节点中。遍历任一容器会自动将钩子和Foo 内容(至少部分)提取到缓存中,这使得内存访问速度更快。这种方法还有其他好处,例如无需昂贵的元素查找即可在两个容器的迭代器之间切换。

【讨论】:

以上是关于C++ STL - 容器实现的主要内容,如果未能解决你的问题,请参考以下文章

C++提高编程STL-deque容器

c++——STL容器之vector的使用和模拟实现

详解C++ STL priority_queue 容器

(C++ 继承)在 stl 容器中存储具有共同父对象的对象

小白学习C++ 教程二十二C++ 中的STL容器stackqueue和map

小白学习C++ 教程二十二C++ 中的STL容器stackqueue和map