intrusive list介绍
  ZXL8ALkrtBPG 2023年11月02日 67 0

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

intrusive list介绍_CPP

上图中申请的内存为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);

intrusive list介绍_List_02

成员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);

intrusive list介绍_List_03

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;(仅供参考)

intrusive list介绍_CPP_04

总结

侵入式容器由于内存管理是由外部决定的,就可能存在容器和元素生命周期不一致的情况,出现元素的的销毁导致容器的访问失败,因此元素生命周期需要比侵入式容器的要长【1】;

另外还有intrusive ptr:用户已有一套引用计数管理机制,可使用intrusive ptr来封装达到智能指针的效果;

参考资料

【1】《Boost程序库探秘》

【2】boost源码

【3】https://github.com/martinus/nanobench

【4】https://nanobench.ankerl.com/tutorial.html#usage

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  ZXL8ALkrtBPG   2023年11月02日   45   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   64   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   38   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   45   0   0 cpp
  lh6O4DgR0ZQ8   2023年11月02日   53   0   0 javaListjson
  ZXL8ALkrtBPG   2023年11月19日   16   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   68   0   0 Listcpp
  ZXL8ALkrtBPG   2023年11月19日   29   0   0 cpp
ZXL8ALkrtBPG