intrusive list意为侵入式链表,而这里的侵入式是对list中元素代码的适度改造,为intrusive container的其中一种容器;与之相对应的是非侵入式链表,stl中的容器都属于非侵入式容器,非侵入式容器不需要元素进行任何修改因此更加方便,但同时在性能上会有存在一些trade off。
std::list 内存申请
stl中list为顺序容器,迭代器类型为双向迭代器,内部实现为双向环形链表,其负责元素的内存分配,通过简单测试:
class Person {
public:
Person()
{
std::cout << "Person Cons" << std::endl;
}
Person(const Person &)
{
std::cout << "Person Copy Cons" << std::endl;
}
private:
int Age;
int Height;
int weight;
};
void *operator new(size_t size)
{
void *ptr = malloc(size);
std::cout << "new size:" << size << std::endl;
return ptr;
}
int main()
{
std::list<Person> s;
Person p {};
s.push_back(p);
return 0;
}
// 输出
Person Cons
new size:32
Person copy Cons
上图中申请的内存为std::List_node<Person>,查看List_node源码:
template <class _Value_type, class _Voidptr>
struct _List_node { // list node
using value_type = _Value_type;
using _Nodeptr = _Rebind_pointer_t<_Voidptr, _List_node>;
_Nodeptr _Next; // successor node, or first element if head
_Nodeptr _Prev; // predecessor node, or last element if head
_Value_type _Myval = // the stored value, unused if head
_Returns_exactly<_Value_type>();
...
};
内存申请包含了元素本身 _Myval + _Next + _Prev,内存对齐后共32个字节,因此多次插入每次都会申请32个字节,节点之间的内存不连续,同时在每次插入链表时会产生一次拷贝构造;
将上述代码修改为:
int main()
{
std::list<Person*> s;
Person p {};
s.push_back(&p);
return 0;
}
此时list中对每个元素需要申请 _Myval (Person*)+ _Next + _Prev,64位下三个指针共24个字节,与元素Persion本身内存为两块内存;
在嵌入式系统中内存的动态申请往往会造成不少的性能开销,通常是预先申请再划分以提升性能,同时连续内存可以减少cache miss提升内存访问性能(locality);为解决此问题,可以使用intrusive list进行优化;
intrusive list
常见双向链表元素的实现方式为:
class Person {
Person *Prev = nullptr;
Person *Next = nullptr;
int Age = 0;
int Height = 0;
int weight = 0;
public:
Person *SetNext(PersionNode *p)
{
this->Next = p;
p->Prev = this;
}
};
其中Prev 、Next为实现链表而在元素中额外增加的存储空间和实现,这种即为一种侵入式链表的元素,使用上:
Person p, p1;
p.SetNext(&p1); // 相比std::list,在一块连续内中操作
侵入式链表本身负责链表的指针操作,即push_back、push_front、size等,但不负责元素的内存分配。相比非侵入式链表,元素和指针管理(Prev和Next)在同一内存中;
和上文中std::list<Person*>对比减少了cache miss,同时由于容器本身和元素是一种聚合关系,容器不负责管理元素的生命周期;实现侵入式链表需要修改自定义类型,有两种方式进行侵入:基类继承和组合成员;两者称为hook挂钩;
基类hook
以boost库中侵入式单链表为例:
class Person : public slist_base_hook<> {
...
};
using namespace boost::intrusive;
Person p {};
slist<Person> s;
s.push_front(p);
EXPECT_EQ(&s.front(), &p);
成员hook
class Person {
public:
...
slist_member_hook<> m_hook;
};
Person p {};
slist<Person, member_hook<Person, slist_member_hook<>, &Person::m_hook>> s;
s.push_front(p);
EXPECT_EQ(&s.front(), &p);
hook中均使用了默认策略,可以在hook模板实参指定不同的策略;
Benchmark【4】
#define DATA_SIZE (1000)
std::vector<Person> v(DATA_SIZE, Person{});
std::vector<PersonIntrusive> vi(DATA_SIZE, PersonIntrusive{});
ankerl::nanobench::Bench().run("std::list", [&] {
std::list<Person*> l;
for (int i = 0; i < DATA_SIZE; i++) {
l.push_back(&v[i]);
}
for (auto& p : l) {
}
});
ankerl::nanobench::Bench().run("intrusive list", [&] {
List<PersonIntrusive> l;
for (int i = 0; i < DATA_SIZE; i++) {
l.pushBack(vi[i]);
}
for (auto& p : l) {
}
});
简单对比两种list,预先通过vector申请好堆内存,然后插入list并且遍历:
std::list:181300ns,intrusive list(基类hook):28108ns;(仅供参考)
总结
侵入式容器由于内存管理是由外部决定的,就可能存在容器和元素生命周期不一致的情况,出现元素的的销毁导致容器的访问失败,因此元素生命周期需要比侵入式容器的要长【1】;
另外还有intrusive ptr:用户已有一套引用计数管理机制,可使用intrusive ptr来封装达到智能指针的效果;
参考资料
【1】《Boost程序库探秘》
【2】boost源码