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:[email protected] 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 內核之中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章