Nginx中hash表的設計與實現

在nginx中使用的hash中一個很是核心的函數就是ngx_hash_init,因爲nginx這個hash表是靜態只讀的,即不能在運行時動態添加新元素的,一切的結構和數據都在配置初始化的時候就已經規劃完畢,因此「init」過程的優劣,對運行時查找的性能影響很是大。在正式分析以前,下面的這個鏈接給出了一個很是詳細的hash結構的總體佈局,對理解代碼幫助會很大,必定要仔細看一下。 http://code.google.com/p/nginxsrp/wiki/NginxCodeReview#ngx_hash nginx

先思考這樣一個問題,假設讓你來設計一個靜態的hash表,對於一批肯定數量的關鍵字,如何創建一個合理而且高效的hash表,讓運行時的查找足夠高效呢?你的hash表槽位多少合適?key衝突問題如何解決,用鏈式?鏈表長度該如何肯定,太長效率低,那麼多長合適?想一想這些問題,而後咱們看看nginx是如何去作的。 數組

ngx_int_t  
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)  
{  
    u_char          *elts;  
    size_t           len;  
    u_short         *test;  
    ngx_uint_t       i, n, key, size, start, bucket_size;  
    ngx_hash_elt_t  *elt, **buckets;  
      
    /* nelts是關鍵字的數量,bucket_size爲一個bucket的大小,這裏注意的就是一個bucket至少能夠容得下一個關鍵字, 
     * 而下面的NGX_HASH_ELT_SIZE(&name[n] + sizeof(void *))正好就是一個關鍵字所佔的空間。 
     * 經過判斷條件來看,若是咱們設定的bucket大小,必須保證能容得下任何一個關鍵字,不然,就報錯,提示bucket指定的過小。 
     * 關於NGX_HASH_ELT_SIZE這個宏,這裏提一下,nginx把因此定位到某個bucket的關鍵字,即衝突的,封裝成ngx_hash_elt_t結構 
     * 挨在一塊兒放置,這樣組成了一個ngx_hash_elt_t數組,這個數組空間的地址,由ngx_hash_t中的buckets保存。對於某個關鍵字來講, 
     * 它有一個ngx_hash_elt_t的頭結構和緊跟在後面的內容組成,從這個角度看一個關鍵字所佔用的空間正好等於NGX_HASH_ELT_SIZE宏的值 
     * 只是裏面多了一個對齊的動做。 
     */  
    for (n = 0; n < nelts; n++) {  
        /*  
         * 這裏考慮放置每一個bucket最後的null指針所須要的空間,即代碼中的sizeof(void *),這個NULL在find過程當中做爲一個bucket 
         * 的結束標記來使用。 
         */  
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))  
        {  
            return NGX_ERROR;  
        }  
    }  
      
    /* max_size是bucket的最大數量, 這裏的test是用來作探測用的,探測的目標是在當前bucket的數量下,衝突發生的是否頻繁。 
     * 過於頻繁則說明當前的bucket數量過少,須要調整。那麼如何斷定衝突過於頻繁呢?就是利用這個test數組,它總共有max_size個 
     * 元素,即最大的bucket。每一個元素會累計落到該位置關鍵字長度,當大於256個字節,即u_short所表示的最大大小時,則斷定 
     * bucket過少,引發了嚴重的衝突。後面會看到具體的處理。 
     */  
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);  
    if (test == NULL) {  
        return NGX_ERROR;  
    }  
      
    /* 每一個bucket的末尾一個null指針做爲bucket的結束標誌, 這裏bucket_size是容納實際數據大小,故減去一個指針大小 */  
    bucket_size = hinit->bucket_size - sizeof(void *);  
      
    /*  
     * 這裏考慮NGX_HASH_ELT_SIZE中,因爲對齊的緣故,一個關鍵字最少須要佔用兩個指針的大小。 
     * 在這個前提下,來估計所須要的bucket最小數量,即考慮元素越小,從而一個bucket容納的數量就越多, 
     * 天然使用的bucket的數量就越少,但最少也得有一個。 
     */  
    start = nelts / (bucket_size / (2 * sizeof(void *)));  
    start = start ? start : 1;  
      
    /*  
     * 調整max_size,即bucket數量的最大值,依據是:bucket超過10000,且總的bucket數量與元素個數比值小於100 
     * 那麼bucket最大值減小1000,至於這幾個判斷值的由來,尚不清楚,經驗值或者理論值。 
     */  
    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {  
        start = hinit->max_size - 1000;  
    }  
      
    /* 在以前肯定的最小bucket個數的基礎上,開始探測(經過test數組)並根據須要適當擴充,前面有分析其原理 */  
    for (size = start; size < hinit->max_size; size++) {  
  
        ngx_memzero(test, size * sizeof(u_short));  
  
        for (n = 0; n < nelts; n++) {  
            if (names[n].key.data == NULL) {  
                continue;  
            }  
  
            key = names[n].key_hash % size;  
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
  
            if (test[key] > (u_short) bucket_size) {  
                goto next;  
            }  
        }  
  
        goto found;  
          
    next:  
        /* 到next這裏,就是實際處理bucket擴充的狀況了,即遞增表示bucket數量的size變量 */  
        continue;  
    }  
  
    ngx_free(test);  
  
    return NGX_ERROR;  
  
found:  
    /* 肯定了合適的bucket數量,即size。 從新初始化test數組,初始值爲一個指針大小。*/  
    for (i = 0; i < size; i++) {  
        test[i] = sizeof(void *);  
    }  
      
    /* 統計各個bucket中的關鍵字所佔的空間,這裏要提示一點,test[i]中除了基本的數據大小外,還有一個指針的大小 
     * 如上面的那個for循環所示。 
     */  
    for (n = 0; n < nelts; n++) {  
        if (names[n].key.data == NULL) {  
            continue;  
        }  
  
        key = names[n].key_hash % size;  
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
    }  
  
    len = 0;  
      
    /* 調整成對齊到cacheline的大小,並記錄全部元素的總長度 */  
    for (i = 0; i < size; i++) {  
        if (test[i] == sizeof(void *)) {  
            continue;  
        }  
  
        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));  
  
        len += test[i];  
    }  
      
    /*  
     * 申請bucket元素所佔的空間,這裏注意的一點就是,若是以前hash表頭結構沒有申請, 
     * 那麼在申請時將ngx_hash_wildcard_t結構也一塊兒申請了。 
     */  
    if (hinit->hash == NULL) {  
        hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)  
                                             + size * sizeof(ngx_hash_elt_t *));  
        if (hinit->hash == NULL) {  
            ngx_free(test);  
            return NGX_ERROR;  
        }  
  
        buckets = (ngx_hash_elt_t **)  
                      ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));  
  
    } else {  
        buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));  
        if (buckets == NULL) {  
            ngx_free(test);  
            return NGX_ERROR;  
        }  
    }  
  
    elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);  
    if (elts == NULL) {  
        ngx_free(test);  
        return NGX_ERROR;  
    }  
  
    elts = ngx_align_ptr(elts, ngx_cacheline_size);  
      
    /* 設置各個bucket中包含實際數據的空間的地址(或者說位置) */  
    for (i = 0; i < size; i++) {  
        if (test[i] == sizeof(void *)) {  
            continue;  
        }  
  
        buckets[i] = (ngx_hash_elt_t *) elts;  
        elts += test[i];  
  
    }  
      
    /* 用來累計真實數據的長度,不計結尾指針的長度 */  
    for (i = 0; i < size; i++) {  
        test[i] = 0;  
    }  
      
    /* 依次向各個bucket中填充實際的內容,代碼沒什麼好分析的。*/  
    for (n = 0; n < nelts; n++) {  
        if (names[n].key.data == NULL) {  
            continue;  
        }  
  
        key = names[n].key_hash % size;  
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);  
  
        elt->value = names[n].value;  
        elt->len = (u_short) names[n].key.len;  
  
        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);  
        /* test[key]記錄當前bucket內容的填充位置,即下次填充的開始位置 */  
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
    }  
      
    /* 設置bucket結束位置的null指針,*/  
    for (i = 0; i < size; i++) {  
        if (buckets[i] == NULL) {  
            continue;  
        }  
        /*  
         * 因爲前面bucket的處理中多留出了一個指針的空間,而此時的test[i]是bucket中實際數據的共長度, 
         * 因此bucket[i] + test[i]正好指向了末尾null指針所在的位置。處理的時候,把它當成一個ngx_hash_elt_t結構看, 
         * 在該結構中的第一個元素,正好是一個void指針,咱們只處理它,別的都不去碰,因此沒有越界的問題。 
         */  
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);  
  
        elt->value = NULL;  
    }  
  
    ngx_free(test);  
  
    hinit->hash->buckets = buckets;  
    hinit->hash->size = size;  
  
    return NGX_OK;  
}
相關文章
相關標籤/搜索