复用tcmalloc源码中的基数树进行优化

  • Post author:
  • Post category:其他




基数树(radix tree)


基数树:


  • 压缩前缀树,是一种更节省空间的Trie(前缀树)。对于基数树的每个节点,如果该节点是确定的子树的话,就和父节点合并。基数树可用来构建关联数组。 用于IP 路由。 信息检索中用于文本文档的倒排索引

  • 基数树将长整数与指针键值相关联,解决Hash冲突和Hash表大小的设计问题

  • 利用基数树可以根据一个长整型(比如一个长ID,或一串地址)快速查找到其对应的对象指针(在redis中用来存储stream消息队列,tcmalloc中用来建立page id和span*的映射)

  • 基数树的空间使用更加灵活,只有当需要用到某节点时才会去创建它(pageMap)


基数树的插入查找:


  • 插入

    在这里插入图片描述

  • 查找:


    在这里插入图片描述

  • 删除 分为两种:



    惰性删除:即沿着路径查找到叶节点后,直接删除叶节点,中间的非叶节点不删除



    非惰性删除:将删除后只有一条边的顶点向上合并


基数树的缺点:


  • 基数树另一个主要的缺陷是低效。即使你只想存一个键值对,但其中的键长度有几百字符长,那么每个字符的那个层级你都需要大量的额外空间。每次查找和删除都会有上百个步骤。可以使用Patricia树来解决这个问题(参考:

    Merkle Patricia Tree(MPT)




tcmalloc中的基数树



PageMap



TCMalloc中针对32、64位系统设计了不同基数树(考虑这个映射表占用内存的大小):


  • 32位系统使用二级的Radix Tree(PageMap2),Two-level radix tree

  • 64位系统使用三级Radix Tree(PageMap3),Three-level radix tree

  • PageMap负责判断并调用PageMap2与PageMap3



PageMap2(Two-level radix tree)

:

(叶节点(无论指针大小)总是映射 2

15

个条目;对于 8K 的页,为每个叶节点提供了 256MB 的映射)


entries


  • 非类型模板参数BITS保存了记录

    存储所有页所需要的比特位

    (32位下是32 -TCMALLOC_PAGE_SHIFT, 64位下48 -TCMALLOC_PAGE_SHIFT)


    在这里插入图片描述

  • TCmalloc中TCMALLOC_PAGE_SHIFT的值分多种情况:


    在这里插入图片描述

  • 以32位下TCMALLOC_PAGE_SHIFT=13为例:



    BITS为32-13=19,kLeafBits=15(叶节点总是映射 2

    15

    个条目)kLeafLength=32768字节,kRootBits根节点=19-5=4((1<<kRootBits) 不能溢出“int”) kRootLength=16字节

  • 叶节点Leaf定义在PageMap2里面

    在这里插入图片描述


    PageMap2私有一个Leaf*的数组,Leaf私有一个Span*的数组

  • 共19页将高4位作为root_[],而低15位作为Leaf[](???Leafbits为什么是15??应该root 5 leaf 14)




    如图:



    在这里插入图片描述



PageMap3(Three-level radix tree):




对于 x86有 48 个可用位(__x86_64__处理器中,只用了地址的低48bits用于虚拟和物理地址的转换,高16bits是不用的),对于 PowerPC,只有 46 个可用位(这里不做讨论)


在这里插入图片描述


  • 此处BITS为48-13=35,与二级类似,不做赘述,只是增加了一个struct Node作为Mid-level


    在这里插入图片描述


    分为三级,第一级root_[]占12位,第二级leafs[]占12位,第三级span[]占11位


  • 如图所示:



    在这里插入图片描述



get与set,Ensure


对于PageMap2:


在这里插入图片描述

在这里插入图片描述


  • 页号为k,i1为root_[]中的下标,i2为span中的下标,映射关系如下

  • 对于i1, kLeafBits=15,右移kLeafBits位,取k高4位即kRootBits位,作为root_[]找span[]的下标

    (4-15??5-14??)


  • 对于i2,(kLeafLength-1)(2

    15

    -1)就是将第15位值0,其他位(0-14)置1,相&,即取k低15位作为span[]的下标


对于PageMap3:


在这里插入图片描述

在这里插入图片描述


  • 同理,移位操作i1,i2,i3作为root_[], leafs[], span[]的下标



对于PagaeMap2与PageMap3都有一个Ensure函数用于确认以及开辟空间:



在这里插入图片描述

在这里插入图片描述


  • 第一层root_[]都是提前开好的,注释可见,而成,三层都是在需要时才开辟空间,减少元数据的内存占用


其他函数如get_next_set_page(), 获取下一个set好的页 等这里不做讨论



使用基数树为什么可以不用加锁


之前使用unordered_map时可能有的线程在建立映射,有的线程在使用MapToSpan()进行读取

(必须加锁)


  • 若使用set,底层是红黑树:遍历时如果正好在进行旋转(左右旋),映射关系发生变化,出错

  • 若使用unordered_map, 底层是哈希桶,遍历时刚好在进行扩容或插入删除,桶也变了,错误


而使用基数树:


  • 插入时不会动结构,在写之前会提前开好空间(红黑树或哈希桶会动结构)

  • 只有两个函数FetchNewSpan和ReleaseSpanToPageCache会使用_pageMap建立映射,多个线程不可能对同一个位置进行写(调用这两个函数时已加锁)

  • 读写是分离的,不可能读的时候在写



改造PageMap并复用


增加了一个一层的PageMap1(采用直接定值法哈希(提前开好))



PageMap1 (One-level radix tree)


  • 私有一个指针数组的指针array_

  • get,set操作很简单(就是直接定值法哈希)

  • 源码中使用了Allocator空间配置器开辟空间,本质上也是使用了malloc,为了脱离使用,直接使用SystemAlloc开辟空间


对于PageMap2,PageMap3:


  • 源码中使用了Allocator空间配置器开辟空间,本质上也是使用了malloc,为了脱离使用,这里使用定长内存池ObjectPool来开辟空间

  • 只留下get set Ensure这三个主体函数



vs studio改为x86, 复用PageMap2,替换映射函数,修改PageCache私有_pageMap

#ifdef _WIN64
	PageMap3<48 - PAGE_SHIFT> _pageMap;//64位下使用PageMap3
#else
	PageMap2<32 - PAGE_SHIFT> _pageMap;//32位下使用PageMap2
#endif
	//std::unordered_map<PAGE_ID, Span*> _pageMap;



再次对比性能:



在这里插入图片描述



性能明显提高



VS Studio进行性能瓶颈分析



未使用基数树之前:





使用调试中的性能探测器:



在这里插入图片描述



使用基数树进行优化后




lock不再是独占时间最多的

在这里插入图片描述



  • 点入函数名称可以看到细节:



    在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述



版权声明:本文为qq_41420788原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。