radix tree 整理

1、(HOW)这是个什么玩意,可以用来干嘛?
Linux radix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的页。Linux radix树的API函数在lib/radix-tree.c中实现。
Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR(ID RADIX TREE)机制)、内存管理等。
上图显示了一个有3级结点的radix树,每个数据条目(item)可用3个6位的键值(key)进行索引,键值从左到右分别代表第1~3层结点位置。没有孩子的结点在图中不出现。因此, radix树为稀疏树提供了有效的存储,代替固定尺寸数组提供了键值到指针的快速查找。 
以index=0x5BFB68为例,化为二进制,每6位为一组:10110(22,第一层编号) 111111(63第二次编号) 101101(45第三层编号) 101000(40第四层编号)

简而言之,这个算法就是一种索引。
2、(what) 这个算法怎么用,时间复杂度如何?
     同hash算法一样的操作,插入,删除,查找。radix算法的时间查询时间复杂度取决于radix tree的高度
Redis,Memcached等作为Key—Value 存储的模型的数据路由都采用Hash表来达到目的。如何解决Hash冲突和Hash表大小的设计是一个很头疼的问题。
借助于Radix树,我们同样可以达到对于uint32_t 的数据类型的路由。这个灵感就来自于Linux内核的IP路由表的设计。
3、(why)这个算法的实现过程是怎么样的? 

那我们简单介绍一下Radix树:

Radix Tree(基树) 其实就差不多是传统的二叉树,只是在寻找方式上,利用比如一个unsigned int  的类型的每一个比特位作为树节点的判断。

可以这样说,比如一个数  1000101010101010010101010010101010 (随便写的)那么按照Radix 树的插入就是在根节点,如果遇到 0 ,就指向左节点,如果遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。如果觉得太多的调用Malloc的话,可以采用池化技术,预先分配多个节点,本博文就采用这种方式。

复制代码
 1 typedef struct _node_t
 2 {
 3     char     zo                ;         // zero or one 4     int        used_num       ;
 5     struct _node_t *parent ;
 6     struct _node_t *left   ;
 7     struct _node_t *right  ;
 8     void            *data   ;//for nodes array list finding next empty node 9     int        index           ;
10 }mc_radix_node_t ;
复制代码

节点的结构定义如上。

zo 可以忽略,父节点,坐指针,右指针顾名思义,data 用于保存数据的指针,index 是作为 node 池的数组的下标。

 

树的结构定义如下:

复制代码
 1 ypedef struct _radix_t
 2 {
 3     mc_radix_nodes_array_t * nodes    ;
 4     mc_radix_node_t    *         root      ;
 5 
 6     mc_slab_t        *         slab      ;
 7     
 8     
 9     /*10     pthread_mutex_t             lock        ;
11     */12     int                         magic       ;
13     int                         totalnum ;
14     size_t                     pool_nodenum ;
15     
16     mc_item_queue             queue ;
17 }mc_radix_t ;
复制代码

 暂且不用看 nodes 的结构,这里只是作为一个node池的指针

 root 指针顾名思义是指向根结构,slab 是作为存放数据时候的内存分配器,如果要使用内存管理来减少开销的话(参见slab内存分配器一章)

 magic用来判断是否初始化,totalnum 是叶节点个数,poll_nodenum 是节点池内节点的个数。

 queue是作为数据项中数据的队列。

 

我们采用8421编码的宏来作为每一个二进制位的判断:

?
1
2
3
4
#define U01_MASK    0x80000000
#define U02_MASK    0x40000000
#define U03_MASK    0x20000000
#define U04_MASK    0x10000000<br>.<br>.<br>.<br>.

  #define U31_MASK 0x00000002
  #define U32_MASK 0x00000001

 类似这样的方式来对每一位二进制位做判断,还有其他更好的办法,这里只是作为简化和快速。

?
1
2
3
4
5
6
unsigned  int  MASKARRAY[32] = {
     U01_MASK,U02_MASK,U03_MASK,U04_MASK,U05_MASK,U06_MASK,U07_MASK,U08_MASK,
     U09_MASK,U10_MASK,U11_MASK,U12_MASK,U13_MASK,U14_MASK,U15_MASK,U16_MASK,
     U17_MASK,U18_MASK,U19_MASK,U20_MASK,U21_MASK,U22_MASK,U23_MASK,U24_MASK,
     U25_MASK,U26_MASK,U27_MASK,U28_MASK,U29_MASK,U30_MASK,U31_MASK,U32_MASK
};

  

我们为Radix 提供了一些静态函数,不对外声明:

初始化节点池

?
1
static  int  mc_radix_nodes_ini(mc_radix_nodes_array_t *par_nodearray , size_t  par_maxnum )

取得一个节点:

?
1
static  mc_radix_node_t *mc_get_radix_node(mc_radix_nodes_array_t *par_nodearray )

归还一个节点:

?
1
static  void  mc_free_radix_node( mc_radix_nodes_array_t *par_nodearray , mc_radix_node_t * par_free_node )

 这里是初始化radix 树:


 1 int mc_radix_hash_ini(mc_radix_t *t ,size_t nodenum )
 2 {
 3     /* init the node pool */ 4     t->nodes = (mc_radix_nodes_array_t *)malloc( sizeof(mc_radix_nodes_array_t) ); //为节点池分配空间
 5     t->slab = mc_slab_create();                                //使用slab分配器
 6     mc_radix_nodes_ini( t->nodes , nodenum );                      //初始化节点
 7     t->magic = MC_MAGIC ;
 8     t->totalnum = 0 ;
 9     t->pool_nodenum = nodenum ;
10     t->root = NULL ;
11     
12     
13     t->queue.head = NULL ;
14     t->queue.pear = NULL ;
15     t->queue.max_num = nodenum ;
16     t->queue.cur_num = 0 ;
17 }
复制代码
复制代码
 1 int mc_radix_hash_insert( mc_radix_t *t , unsigned int hashvalue , void *data ,size_t size )
 2 {
 3     unsigned int i = 0 ;
 4     mc_radix_node_t * root = t->root ;
 5 
 6     if( t->root == NULL )
 7     {
 8         t->root = mc_get_radix_node( t->nodes ) ;
 9     }
10     
11     /* LRU */12     /*其中涉及到LRU算法,原理是将所有的叶子节点链接为双向队列,然后更新和插入放入队列头,按照一定的比例从队列尾删除数据*/
13     if( t->queue.cur_num >= (t->queue.max_num)*PERCENT )
14     {
15         for( i = 0 ; i < (t->queue.max_num)*(1-PERCENT) ; i++ )
16         {
17             mc_del_item( t , t->queue.pear );
18         }
19     }
20     mc_radix_node_t * cur = t->root ;
21     for(i = 0  ; i < 32 ; i++ )
22     {
23         /* 1 ---> right */24      /*按位来探测树节点*/
25         if( hashvalue & MASKARRAY[i] )
26         {
27             
28             if( cur -> right != NULL )
29             {
30                 cur->used_num++     ;
31                 cur->right->parent = cur ;
32                 cur = cur->right ;                
33             }
34             else35             {
36                 cur->right = mc_get_radix_node( t->nodes ) ;
37                 if( cur->right == NULL )
38                 {
39                     fprintf(stderr,"mc_get_radix_node error\n");
40                     return -1;
41                 }
42                 cur->used_num++     ;
43                 cur->right->parent = cur ;
44                 cur = cur->right ;
45             }
46         }
47         /* 0 ---> left */48         else49         {
50             
51             if( cur->left != NULL )
52             {
53                 cur->used_num++;
54                 cur->left->parent = cur  ;
55                 cur = cur->left ;
56             }
57             else58             {
59                 cur->left = mc_get_radix_node( t->nodes ) ;
60                 if( cur->left == NULL )
61                 {
62                     fprintf(stderr,"mc_get_radix_node error\n");
63                     return -1;
64                 }
65     
66                 cur->used_num++;
67                 cur->left->parent = cur  ;
68                 cur = cur->left ;
69             }
70         }        
71     }
72     
73     t->totalnum ++ ;
74     mc_slot_t * l_slot = mc_slot_alloc( t->slab, size ) ;
75     cur->data = ( mc_slot_t *)(cur->data);
76     memcpy( l_slot->star , data , size );
77     cur->data = l_slot ;
78     
79     /*add to t->queue */80     if( t->queue.head == NULL )
81     {
82         t->queue.head = cur ;
83         t->queue.pear = cur ;
84         cur->left = NULL  ;
85         cur->right = NULL ;
86         
87         t->queue.cur_num++ ;
88     }
89     else90     {
91         cur->left = NULL ;
92         cur->right = t->queue.head ;
93         t->queue.head->left = cur ;
94         t->queue.head = cur ;
95         
96         t->queue.cur_num++ ;
97     }
98     return 1;
99 }

 

删除一个节点,通过hashvalue作为其value,顾名思义


 1 int mc_radix_hash_del( mc_radix_t *t , unsigned int hashvalue )
 2 {
 3     if( t == NULL || t->root == NULL )
 4     {        
 5         return -1;
 6     }
 7     /* non  initialized */ 8     if( t->magic != MC_MAGIC )
 9     {        
10         return -1;
11     }
12     mc_radix_node_t * cur = t->root ;    
13     mc_radix_node_t * cur_par ;
14     int    i = 0 ;
15     for( ; i < 32 ; i++ )
16     {
17         if( hashvalue & MASKARRAY[i] )
18         {
19             
20             if( cur->right != NULL )
21             {
22                 cur->used_num--  ;
23                 cur = cur->right ;
24             }
25             else26                 return -1;
27         }
28         else29         {
30         
31             if( cur->left != NULL )
32             {
33                 cur->used_num-- ;
34                 cur = cur->left ;
35             }
36             else37                 return -1;
38         }
39     }
40     
41     if( cur->used_num >= 0 )
42         mc_slot_free(cur->data);
43     
44     /*remove from t->queue */45     if( cur == t->queue.pear && cur == t->queue.head )
46     {
47         t->queue.pear = NULL ;
48         t->queue.head = NULL ;
49         t->queue.cur_num -- ;
50     }
51     /* the last item */52     else if( cur == t->queue.pear && cur != t->queue.head)
53     {
54         cur->left->right = NULL  ;
55         cur->left = NULL  ;
56         t->queue.cur_num -- ;
57     }
58     else if( cur != t->queue.pear )
59     {
60         cur->left->right = cur->right ;
61         cur->right->left = cur->left ;
62         t->queue.cur_num -- ;
63     }
64     else65     {
66         cur->left->right = cur->right ;
67         cur->right->left = cur->left ;
68         t->queue.cur_num -- ;
69     }
70         
71     for(;;)
72     {
73         
74         if( cur->used_num == 0 )
75         {
76             cur_par = cur->parent ;
77             mc_free_radix_node( t->nodes , cur );
78             cur = cur_par ;
79         }
80         if( cur == NULL )
81             break ;
82         if( cur->used_num > 0  )
83             break ;
84             
85     }
86     
87     return 1;
88     
89 }

 

取得值:通过void * 指向


 1 void *mc_radix_hash_get( mc_radix_t *t , unsigned int hashvalue )
 2 {
 3     if( t == NULL || t->root == NULL )
 4     {        
 5         fprintf(stderr,"t == NULL || t->root == NULL\n");
 6         return (void *)(0);
 7     }
 8     /* non  initialized */ 9     if( t->magic != MC_MAGIC )
10     {        
11         fprintf(stderr,"t->magic != MC_MAGIC\n");
12         return (void *)(0);
13     }
14     mc_radix_node_t * cur = t->root ;    
15     mc_slot_t *ret_slot ;
16     int i = 0 ; 
17     for( ; i < 32 ; i++ )
18     {
19         if( hashvalue & MASKARRAY[i] )
20         {
21             if( cur->right == NULL )
22                 break;
23             else24                 cur = cur->right ;
25         }
26         else27         {
28             if( cur->left == NULL )
29                 break;
30             else31                 cur = cur->left ;
32         }
33     }
34     if( i == 32 )
35     {
36         ret_slot = cur->data;
37         
38         /* update LRU queue*/39         if( cur->left != NULL )
40         {
41             if( cur->right != NULL )
42             {
43                     cur->left->right = cur->right ;
44                     cur->right->left = cur->left ;
45                     cur->left = t->queue.head ;
46                     t->queue.head->left = cur ;
47                     t->queue.head = cur ;
48             }
49             else50             {
51                 /* cur->right == NULL  last element of LRU queue */52                     cur->left->right = NULL ;
53                     cur->left = t->queue.head ;
54                     t->queue.head->left = cur ;
55                     t->queue.head = cur ;
56                     
57             }
58         }
59         return (void *)(ret_slot->star) ;
60     }
61     else62     {
63         fprintf(stderr,"i = %d \n",i);
64         return (void *)(0);
65     }
66 }


 1 int mc_free_radix( mc_radix_t *t )
 2 {
 3     mc_free_all_radix_node(t->nodes);
 4     mc_slab_free(t->slab);
 5     free(t->nodes);
 6 }
 7 
 8 static void mc_del_item( mc_radix_t *t ,  mc_radix_node_t * cur )
 9 {
10     if( cur->left == NULL )
11     {
12         fprintf(stderr,"item number in LRU queue is too small \n");
13         return ;
14     }
15     if( cur->right != NULL )
16     {
17         fprintf(stderr,"cur should be the last of LRU queue \n");
18     }
19     /* remove from LRU queue */20     mc_radix_node_t * pcur = cur->left ;
21     cur->left = NULL   ;
22     pcur->right = NULL ;
23     
24     pcur = cur->parent ;
25     /* remove from radix tree */26     while( pcur != NULL )
27     {
28         cur->used_num -- ;
29         if( cur->used_num <=0 )
30         {
31             mc_free_radix_node( t->nodes , cur );
32         }
33         cur = pcur ;
34         pcur = pcur->parent ;
35     } 
36     
37 }

 

总结:radix 树作为key-value 路由最大的好处就是在于减少了hash表的动态和一部分碰撞问题等。还可以在此结构上方便的扩展 LRU算法,淘汰数据等。

如果担心node 的初始化和申请太过于浪费资源,可以采用节点池的方式设计。

文章属原创,转载请注明出处 联系作者: Email:zhangbo1@ijinshan.com QQ:51336447
4、最一反三, 这种算法对比 hash,btree等算法的优点是什么,应用场景有什么区别?

     hash对比:  对于 长整型数据的映射 ,如何解决Hash冲突和Hash表大小的设计是一个很头疼的问题。
radix树就是针对这种稀疏的长整型数据查找,能快速且节省空间地完成映射。借助于Radix树,我们可以 实现对于长整型数据类型的路由 利用radix树可以根据一个长整型(比如一个长ID)快速查找到其对应的对象指针 。这比用hash映射来的简单,也更节省空间,使用hash映射hash函数难以设计,不恰当的hash函数可能增大冲突,或浪费空间。
    AVL树:最早的平衡二叉树之一。应用相对其他数据结构比较少。windows对进程地址空间的管理用到了AVL树

     红黑树:平衡二叉树,广泛用在C++的STL中。map和set都是用红黑树实现的。我们熟悉的STL的map容器底层是RBtree,当然指的不是unordered_map,后者是hash。

     B/B+树用在磁盘文件组织 数据索引和数据库索引

     Trie树 字典树,用在统计和排序大量字符串

------

AVL是一种高度平衡的二叉树,所以通常的结果是,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,
更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果场景中对插入删除不频繁,只是对查找特别有要求,AVL还是优于红黑的。

红黑树的应用就很多了,除了上面同学提到的STL,还有
epoll在内核中的实现,用红黑树管理事件块
nginx中,用红黑树管理timer等
Java的TreeMap实现
著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块

B和B+主要用在文件系统以及数据库中做索引等,比如Mysql:B-Tree Index in MySql

trie 树的一个典型应用是前缀匹配,比如下面这个很常见的场景,在我们输入时,搜索引擎会给予提示
还有比如IP选路,也是前缀匹配,一定程度会用到trie

------

跳表:Redis中就使用跳表,而不是红黑树来存储管理其中的元素(应该说的是一级元素-直接的Key,里面的value应该是有不同的数据结构)。

首先,跳表是skiplist?不是ziplist。ziplist在redis中是一个非常省内存的链表(代价是性能略低),所以在hash元素的个数很少(比如只有几十个),
那么用这个结构来存储则可以在性能损失很小的情况下节约很多内存(redis是内存数据库啊,能省还是要省的)。好这个问题清楚了。

在server端,对并发和性能有要求的情况下,如何选择合适的数据结构(这里是跳跃表和红黑树)。
如果单纯比较性能,跳跃表和红黑树可以说相差不大,但是加上并发的环境就不一样了,
如果要更新数据,跳跃表需要更新的部分就比较少,锁的东西也就比较少,所以不同线程争锁的代价就相对少了,
而红黑树有个平衡的过程,牵涉到大量的节点,争锁的代价也就相对较高了。性能也就不如前者了。
在并发环境下skiplist有另外一个优势,红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,
而skiplist的操作显然更加局部性一些,锁需要盯住的节点更少,因此在这样的情况下性能好一些。
     
红黑树,AVL树简单来说都是用来搜索的呗。

AVL树:平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),
只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况。

红黑树:平衡二叉树,通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。
所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。
(现在部分场景使用跳表来替换红黑树,可搜索“为啥 redis 使用跳表(skiplist)而不是使用 red-black?”)

B树,B+树:它们特点是一样的,是多路查找树,一般用于数据库系统中,为什么,因为它们分支多层数少呗,
都知道磁盘IO是非常耗时的,而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。
B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。

Trie树:
又名单词查找树,一种树形结构,常用来操作字符串。它是不同字符串的相同前缀只保存一份。
相对直接保存字符串肯定是节省空间的,但是它保存大量字符串时会很耗费内存(是内存)。

类似的有
前缀树(prefix tree),后缀树(suffix tree),radix tree(patricia tree, compact prefix tree),crit-bit tree(解决耗费内存问题),
以及前面说的double array trie。
简单的补充下我了解应用
前缀树:字符串快速检索,字符串排序,最长公共前缀,自动匹配前缀显示后缀。
后缀树:查找字符串s1在s2中,字符串s1在s2中出现的次数,字符串s1,s2最长公共部分,最长回文串。
radix tree:linux内核,nginx。



5、来看看nginx是如何利用radix tree的一个例子

本文分析基于Nginx-1.2.6,与旧版本或将来版本可能有些许出入,但应该差别不大,可做参考

radix tree是一种字典树,可以很得心应手地构建关联数组。在信息检索中可用于生成文档的倒排索引,另外,在IP路由选择中也有其特别的用处。

在Nginx中实现了radix tree,其主要用在GEO模块中,这个模块中只有一个指令即geo,通过这个指令可以定义变量,而变量的值依赖于客户端的IP地址(默认使用($remote_addr,但也可设定为其他变量),通过这个模块可以实现负载均衡,对不同区段的用户请求使用不同的后端服务器。一个例子:

 geo  $country  {
   default          no; 
   127.0.0.0/24     us;    #/之前为IP地址address,/之后是地址掩码mask
   127.0.0.1/32     ru;
   10.1.0.0/16      ru;
   192.168.1.0/24   uk;    #当ip地址为192.168.1.23时,变量country的值为uk
 }
nginx在解析上面这段配置时,会构建一个数据结构,并在接受请求后根据客户端IP地址查找对应的变量值,这个数据结构就是radix tree,它是一棵二叉树,其结构图如下所示,每条边对应1bit是0或1。 ![radix tree][1]
01 typedef struct ngx_radix_node_s  ngx_radix_node_t;
02  
03 struct ngx_radix_node_s {
04     ngx_radix_node_t  *right;
05     ngx_radix_node_t  *left;
06     ngx_radix_node_t  *parent;
07     uintptr_t          value;
08 };
09  
10 typedef struct {
11     ngx_radix_node_t  *root;
12     ngx_pool_t        *pool;
13     ngx_radix_node_t  *free;
14     char              *start;
15     size_t             size;
16 } ngx_radix_tree_t;

为避免频繁地为ngx_radix_node_t分配和释放空间,实现节点的复用,ngx_radix32tree_delete删除节点后并没有释放空间,而是利用ngx_radix_tree_t中的成员free把删除的节点连接成了一个单链表结构,在调用ngx_radix_alloc创建新节点时就先看free右孩子指针所指向的链表是否为空,如果不为空,就从中取出一个节点返回其地址。另外,为radix tree分配空间是以Page为单位的,start指向Page中可用内存的起始位置,size是page中剩余可用的空间大小。

radix tree的创建、插入一节点、删除一节点、查找这四个操作的函数声明如下:

1 ngx_radix_tree_t *ngx_radix_tree_create(ngx_pool_t *pool,
2     ngx_int_t preallocate);
3 ngx_int_t ngx_radix32tree_insert(ngx_radix_tree_t *tree,
4     uint32_t key, uint32_t mask, uintptr_t value);
5 ngx_int_t ngx_radix32tree_delete(ngx_radix_tree_t *tree,
6     uint32_t key, uint32_t mask);
7 uintptr_t ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key);

插入节点

geo指令中的“192.168.1.0/24 ru;”这样一条配置就对应了radix tree中的一个节点,那程序中是如何实现的呢?首先看函数ngx_radix32tree_insert中的参数,key是对应inaddrt类型的ip地址转换成主机字节序后的四个字节,mask即网络掩码,对应于24的是0xFFFFFF00四个字节,value是对应ru的一个 ngx_http_variable_value_t类型的指针。

将value插入那个位置呢?从key&mask的最高位开始,若是0,则转向左孩子节点,否则转向右孩子节点,以此类推沿着树的根节点找到要插入的位置(对应上面例子的要插入的节点在第24层)。若到了叶子节点仍没到达最终位置,那么在叶子节点和最终位置之间空缺的位置上插入value=NGX_RADIX_NO_VALUE的节点。如果对应位置已经有值,返回NGX_BUSY,否则设置对应的value,返回NGX_OK。

创建

为radix tree树结构及其root节点分配空间,并根据preallocate的值向树中插入一定数量的节点,当preallocate等于-1时,会重新为preallocate设置适当的值,不同平台下会插入不同数量的节点。

preallocate的具体含义是,在树中插入第1层到第preallocate层所有的节点,即创建树之后树中共有2^(preallocate+1)-1个节点。那么,当preallocate=-1时,应该为不同的平台设定怎样的值呢?这是由num=ngx_pagesize/sizeof(ngx_radix_node_t)决定的,当为num=128时,preallocate=6,这是因为预先插入节点生成的树是完全二叉树,树的第6层节点都插满时,树共有127个节点占用正好不大于1页内存的空间,增加preallocate继续预先插入节点就会得不偿失。这里我也说不太清楚,贴上注释:

01 /*
02  * Preallocation of first nodes : 0, 1, 00, 01, 10, 11, 000, 001, etc.
03  * increases TLB hits even if for first lookup iterations.
04  * On 32-bit platforms the 7 preallocated bits takes continuous 4K,
05  * 8 - 8K, 9 - 16K, etc.  On 64-bit platforms the 6 preallocated bits
06  * takes continuous 4K, 7 - 8K, 8 - 16K, etc.  There is no sense to
07  * to preallocate more than one page, because further preallocation
08  * distributes the only bit per page.  Instead, a random insertion
09  * may distribute several bits per page.
10  *
11  * Thus, by default we preallocate maximum
12  *     6 bits on amd64 (64-bit platform and 4K pages)
13  *     7 bits on i386 (32-bit platform and 4K pages)
14  *     7 bits on sparc64 in 64-bit mode (8K pages)
15  *     8 bits on sparc64 in 32-bit mode (8K pages)
16  */

查找

现在给定一个ip,应该在radix tree中怎样找到对应的变量值呢?首先将ip地址转换成主机字节序的四个字节,然后调用uintptr_t ngx_radix32tree_find即可,在这个函数中,会将从32位的key的最高位开始,若是0,就转向左孩子,若是1,就转向右孩子,这样从树的根节点开始,直到找到对应的叶子节点为止,在此查找路径上最后一个值不为NGX_RADIX_NO_VALUE的node的value就是所返回的值。 代码如下:

01 uintptr_t
02 ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key)
03 {
04     uint32_t           bit;
05     uintptr_t          value;
06     ngx_radix_node_t  *node;
07  
08     bit = 0x80000000;
09     value = NGX_RADIX_NO_VALUE;
10     node = tree->root;
11  
12     while (node) {
13        if (node->value != NGX_RADIX_NO_VALUE) {
14             value = node->value;
15        }
16  
17         if (key & bit) {
18             node = node->right;
19  
20         else {
21             node = node->left;
22         }
23  
24         bit >>= 1;
25     
26  
27     return value;
28  }

删除节点

删除过程,首先要先找到要删除的节点,其过程同插入一节点时相同,如果找不到,返回NGX_ERROR,否则就分两种情况:

  • 如果要删除的节点是叶子节点,那么将此节点删除,并插入到free右孩子指针所指向的链表中,留在以后复用,如果删除之后,其父节点成了叶子节点且其值为NGX_RADIX_NO_VALUE,那么也将其父节点执行同样的删除操作,以此类推直到根节点为止;

  • 如果要删除的节点有至少一个孩子,并且这个要删除的节点的值不是NGX_RADIX_NO_VALUE,则只需设定其值为NGX_RADIX_NO_VALUE即可,这样子处理,减少了删除操作的复杂度,这个节点也只有等遇到第一种情况时才会真正地从树中删除。

最近看代码看到有一个radix tree的应用。引擎对数据建索引时,需要建立字段名到字段序号的映射表,这个表使用非常频繁。比如有6亿document,每个document有100个字段,很多字段字段会同时建index,profile和detail索引,所以需要在表中查找三遍,因此至少需要查找600亿次。如果能提高这些查找的效率,程序的整体效率会得到提高。

写了个小程序对比了下hash_map(Linux平台下的实现)和radix tree的效率。理论上来讲radix tree效率会提高不少,查找一个字符串需要O(n),hash_map需要对字符串求hash值,至少要将字符串遍历一遍,另外还要有一些多余的加减乘除。另外一个影响因素是用C写的代码比较紧凑,使用inline声明比较容易被内联,而hash_map必须使用一个hash函数对象,其本身的代码也比较复杂不容易被内联。radix tree的主要缺点是每个节点的指针数组如果做成动态分配,代码写起来会比较麻烦。析构一个radix tree也比较麻烦。Linux内核也用到了radix tree,没看过代码,应该做的很精致吧。

下面这个例子程序在一台8核8GB内存的RHEL4服务器上运行,hash_map和radix_tree插入相同的16个节点,然后查找1亿次。O2优化后,hash_map运行25s左右,radix tree则只要2s左右,效率提升非常明显。顺便比较了下map,map的查找性能是最差的,要34s。如果要查找600亿次,hash_map需要240分钟,如果分到20个机器上,每个机器起6个线程上,每个线程要花上将近2分钟。可以考虑用oprofile来统计下现在建一次索引花在radix tree查找上的时间。例子程序代码如下。

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#include <iostream>
#include <map>
#include <ext/hash_map>
 
using namespace std;                                                                                 
using namespace __gnu_cxx;
 
namespace __gnu_cxx
{
    template<> struct hash< std::string >
    {
        size_t operator()( const std::string& x ) const
        {
            return hash< const char* >()( x.c_str() );
        }
    };
}
 
#define RADIX_NUM 256
 
struct radix_node_t
{
    radix_node_t* p[RADIX_NUM];
    int index;
    bool is_final;
 
    //radix_node_t() {cout << "new a node" << endl;}
    //~radix_node_t() {cout << "destroy a node" << endl;}
};
 
void radix_init(radix_node_t* &radix)
{
    radix = new radix_node_t;
    memset(radix, 0, sizeof(radix_node_t));
}
 
void radix_insert(radix_node_t* radix, char* str, int index)
{
    char* ptr = str;
    radix_node_t* radix_iter = radix;
 
    while (*ptr)
    {
        //cout << *ptr << endl;
 
        if (radix_iter->p[*ptr] == NULL)
        {
            //
            radix_node_t* new_radix = new radix_node_t;
            memset(new_radix, 0, sizeof(radix_node_t));
            radix_iter->p[*ptr] = new_radix;
        }
 
        radix_iter = radix_iter->p[*ptr];
 
        ++ptr;
    }
 
    radix_iter->index=index;
    radix_iter->is_final=true;  
}
 
inline int radix_find(radix_node_t* radix, char* str)
{
    radix_node_t* radix_iter = radix;
 
    char* ptr = str;
 
    while(*ptr)
    {
        if(radix_iter->p[*ptr] == NULL)
        {
            return -1;
        }
 
        radix_iter = radix_iter->p[*ptr];
        ++ptr;
    }
 
    if (radix_iter->is_final == true)
    {
        return radix_iter->index;
    }
}
 
bool radix_destroy(radix_node_t* radix)
{
    radix_node_t* radix_iter = radix;
    for(int i=0; i<RADIX_NUM; ++i)
    {
        if (radix_iter->p[i] == NULL)
        {
            continue;
        }
 
        //the leaf node
        if (radix_iter->p[i]->is_final == true)
        {
            delete radix_iter->p[i];
        }
        else
        {
            radix_destroy(radix_iter->p[i]);
        }
    }
 
    delete radix_iter; 
}
 
#define FIND_COUNT 100000000
//#define FIND_COUNT 6
#define FIELDS_NUM 16
#define FIELD_LEN 20
 
int main(int argc, const char *argv[])
{
    char fields[FIELDS_NUM][FIELD_LEN]={{"nid"}, {"user_id"}, {"post_fee"}, {"title"}, {"nick"}, {"price"}, {"pict_url"}, {"provcity"}, {"auction_type"}, {"auction_flag"}, {"quantity"}, {"isprepay"}, {"pidvid"}, {"spuid"}, {"promoted_service"}, {"counts"}};
 
    hash_map<string, int> fields_hash_map;
    map<string, int> fields_map;
 
    int begin, end;
 
    //head node
    radix_node_t* radix;
 
    for(int i=0; i<FIELDS_NUM; ++i)
    {
        fields_hash_map[fields[i]] = i;
        fields_map[fields[i]] = i;
    }
 
    begin = time(NULL);
    for (int i=0; i<FIND_COUNT; ++i)
    {
        volatile int index = fields_hash_map[fields[i % FIELDS_NUM]];
    }
    end = time(NULL);
 
    cout << "hash_map time: " << end - begin << endl;
 
    //===================================================
    begin = time(NULL);
    for (int i=0; i<FIND_COUNT; ++i)
    {
        volatile int index = fields_map[fields[i % FIELDS_NUM]];
    }
    end = time(NULL);
 
    cout << "map time: " << end - begin << endl;
 
    //===================================================
 
    radix_init(radix);
 
    for(int i=0; i<6; ++i)
    {
        char* ptr = fields[i];
        radix_insert(radix, ptr, i);
    }
 
    char* str = "abc";
 
    begin = time(NULL);
    for (int i=0; i<FIND_COUNT; ++i)
    {
        char* str = fields[i % FIELDS_NUM];
        volatile int index = radix_find(radix, str);
 
        //cout << index << endl;
    }
    end = time(NULL);
 
    cout << "radix tree time: " << end - begin << endl;
 
    radix_destroy(radix);
 
    return 0;
}

Nginx 中有一个模块:geo,它可以针对不同的 IP 地址来定义不同的变量值,其中就用到了 radix tree 和 red-black tree。

Radix Tree
实质就是 trie 数组的一种变体,但是不同的是其中的边不像 trie 那样只存放一个字符,而是可以存放多个字符。这很有利于路径的压缩,可以有效减小树的深度。radix tree 已经被应用在 bsd 的路由查找和 linux 内核之中。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值