在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; }