程序員必讀: 摸清Hash表的脾性

做者簡介:python

張炎潑(XP)web

白山雲科技合夥人兼研發副總裁,綽號XP。

張炎潑先生於2016年加入白山雲科技,主要負責對象存儲研發、數據跨機房分佈和修復問題解決等工做。以實現100PB級數據存儲爲目標,其帶領團隊完成全網分佈存儲系統的設計、實現與部署工做,將數據「冷」「熱」分離,使冷數據成本壓縮至1.2倍冗餘度。算法

張炎潑先生2006年至2015年,曾就任於新浪,負責Cross-IDC PB級雲存儲服務的架構設計、協做流程制定、代碼規範和實施標準制定及大部分功能實現等工做,支持新浪微博、微盤、視頻、SAE、音樂、軟件下載等新浪內部存儲等業務;2015年至2016年,於美團擔任高級技術專家,設計了跨機房的百PB對象存儲解決方案:設計和實現高併發和高可靠的多副本複製策略,優化Erasure Code下降90%IO開銷。數據結構

軟件開發中,一個hash表至關於把n個key隨機放入到b個bucket中,以實現n個數據在b個單位空間的存儲。
咱們發現hash表中存在一些有趣現象:架構

hash表中key的分佈規律

當hash表中key和bucket數量同樣時(n/b=1):

  • 37% 的bucket是空的
  • 37% 的bucket裏只有1個key
  • 26% 的bucket裏有1個以上的key(hash衝突)

下圖直觀的展現了當n=b=20時,hash表裏每一個bucket中key的數量(按照key的數量對bucket作排序):併發

圖片描述

每每咱們對hash表的第一感受是:若是將key隨機放入全部的bucket,bucket中key的數量較爲均勻,每一個bucket裏key數量的指望是1。app

而實際上,bucket裏key的分佈在n較小時很是不均勻;當n增大時,纔會逐漸趨於平均。函數

key的數量對3類bucket數量的影響

下表表示當b不變,n增大時,n/b的值如何影響3類bucket的數量佔比(衝突率即含有多於1個key的bucket佔比):
圖片描述高併發

更直觀一點,咱們用下圖來展現空bucket率和衝突率隨n/b值的變化趨勢:優化

圖片描述

key數量對bucket均勻程度的影響

上面幾組數字是當n/b較小時有意義的參考值,但隨n/b逐漸增大,空bucket與1個key的bucket數量幾乎爲0,絕大多數bucket含有多個key。

當n/b超過1(1個bucket容許存儲多個key), 咱們主要觀察的對象就轉變成bucket裏key數量的分佈規律。

下表表示當n/b較大,每一個bucket裏key的數量趨於均勻時,不均勻的程度是多少。

爲了描述這種不均勻的程度,咱們使用bucket中key數量的最大值和最小值之間的比例((most-fewest)/most)來表示。

下表列出了b=100時,隨n增大,key的分佈狀況。

圖片描述

能夠看出,隨着bucket裏key平均數量的增長,其分佈的不均勻程度也逐漸下降。

和空bucket或1個key的bucket的佔比不一樣,均勻程度不只取決於n/b的值,也受b值的影響,後面會提到。

未使用統計中經常使用的均方差法去描述key分佈的不均勻程度,是由於軟件開發過程當中,更多時候要考慮最壞狀況下所需準備的內存等資源。

Load Factor:n/b<0.75

hash表中經常使用一個概念 load factor α=n/b,來描述hash表的特徵。

一般,基於內存存儲的hash表,它的 n/b ≤0.75。這樣設定,既可節省空間,也能夠保持key的衝突率相對較低,低衝突率意味着低頻率的hash重定位,hash表的插入會更快。

線性探測是一個常常被使用的解決插入時hash衝突的算法,它在1個bucket出現衝突時,按照逐步增長的步長順序向後查看這個bucket後面的bucket,直到找到1個空bucket。所以它對hash的衝突很是敏感。

在n/b=0.75 這個場景中,若是不使用線性探測(譬如使用bucket內的鏈表來保存多個key),大約有47% 的bucket是空的;若是使用線性探測,這47%的bucket中,大約一半的bucket會被線性探測填充。
在不少內存hash表的實現中,選擇n/b<=0.75做爲hash表的容量上限,不只是考慮到衝突率隨n/b增大而增大,更重要的是線性探測的效率會隨着n/b的增大而迅速下降。

hash表特性小貼士:

  • hash表自己是一個經過必定的空間浪費來換取效率的算法。低時間開銷(O(1))、低空間浪費、低衝突率,三者不可同時兼得;
  • hash表只適合純內存數據結構的存儲:

hash表經過空間浪費換取了訪問速度的提高;磁盤的空間浪費是沒法忍受的,但對內存的少量浪費是可接受的;

hash表只適合隨機訪問快的存儲介質。硬盤上的數據存儲更多使用btree或其餘有序的數據結構。

  • 多數高級語言(內建hash table、hash set等)都保持 n/b≤0.75;
  • hash表在n/b較小時,不會均勻分配key。

Load Factor:n/b>1

另一種hash表的實現,專門用來存儲比較多的key,當 n/b>1時,線性探測失效(沒有足夠的bucket存儲每一個key)。這時1個bucket裏不只存儲1個key,通常在一個bucket內用chaining,將全部落在這個bucket的key用鏈表鏈接起來,來解決衝突時多個key的存儲。

鏈表只在n/b不是很大時適用。由於鏈表的查找須要O(n)的時間開銷,對於很是大的n/b,有時會用tree替代鏈表來管理bucket內的key。

n/b值較大的使用場景之一是:將一個網站的用戶隨機分配到多個不一樣的web-server上,這時每一個web-server能夠服務多個用戶。多數狀況下,咱們都但願這種分配能儘量均勻,從而有效利用每一個web-server資源。

這就要求咱們關注hash的均勻程度。所以,接下來要討論的是,假定hash函數徹底隨機的,均勻程度根據n和b如何變化。

n/b 越大,key的分佈越均勻

當 n/b 足夠大時,空bucket率趨近於0,且每一個bucket中key的數量趨於平均。每一個bucket中key數量的指望是:

**avg=n/b**

定義一個bucket平均key的數量是100%:bucket中key的數量恰好是n/b,下圖分別模擬了 b=20,n/b分別爲 十、100、1000時,bucket中key的數量分佈。

圖片描述

圖片描述

圖片描述

能夠看出,當 n/b 增大時,bucket中key數量的最大值與最小值差距在逐漸縮小。下表列出了隨b和n/b增大,key分佈的均勻程度的變化:

圖片描述

結論:

圖片描述

計算

上述大部分結果來自於程序模擬,如今咱們來解決從數學上如何計算這些數值。

每類bucket的數量

圖片描述

空bucket 數量

對於1個key, 它不在某個特定的bucket的機率是 (b−1)/b

全部key都不在某個特定的bucket的機率是( (b−1)/b)n

已知:

圖片描述

空bucket率是:

圖片描述

空bucket數量爲:

圖片描述

有1個key的bucket數量

n個key中,每一個key有1/b的機率落到某個特定的bucket裏,其餘key以1-(1/b)的機率不落在這個bucket裏,所以,對某個特定的bucket,恰好有1個key的機率是:

圖片描述

恰好有1個key的bucket數量爲:

圖片描述

多個key的bucket

剩下即爲含多個key的bucket數量:

圖片描述

key在bucket中分佈的均勻程度

相似的,1個bucket中恰好有i個key的機率是:n個key中任選i個,並都以1/b的機率落在這個bucket裏,其餘n-i個key都以1-1/b的機率不落在這個bucket裏,即:

圖片描述

這就是著名的二項式分佈

咱們可經過二項式分佈估計bucket中key數量的最大值與最小值。

經過正態分佈來近似

當 n, b 都很大時,二項式分佈能夠用正態分佈來近似估計key分佈的均勻性:

 p=1/b,1個bucket中恰好有i個key的機率爲:

圖片描述

1個bucket中key數量很少於x的機率是:

圖片描述

因此, 全部很少於x個key的bucket數量是:

圖片描述

bucket中key數量的最小值,能夠這樣估算: 若是很少於x個key的bucket數量是1,那麼這惟一1個bucket就是最少key的bucket。咱們只要找到1個最小的x,讓包含很少於x個key的bucket總數爲1, 這個x就是bucket中key數量的最小值。

計算key數量的最小值 x

一個bucket裏包含很少於x個key的機率是:

圖片描述

Φ(x) 是正態分佈的累計分佈函數,當x-μ趨近於0時,可使用如下方式來近似:

圖片描述

這個函數的計算較難,但只是要找到x,咱們能夠在[0~μ]的範圍內逆向遍歷x,以找到一個x 使得包含很少於x個key的bucket指望數量是1。

圖片描述

x能夠認爲這個x就是bucket裏key數量的最小值,而這個hash表中,不均勻的程度能夠用key數量最大值與最小值的差別來描述: 由於正態分佈是對稱的,因此key數量的最大值能夠用 μ + (μ-x) 來表示。最終,bucket中key數量最大值與最小值的比例就是:

圖片描述

(μ是均值n/b)

程序模擬

如下python腳本模擬了key在bucket中分佈的狀況,同時能夠做爲對比,驗證上述計算結果。

import sys
import math
import time
import hashlib

def normal_pdf(x, mu, sigma):

x = float(x)
mu = float(mu)

m = 1.0 / math.sqrt( 2 * math.pi ) / sigma
n = math.exp(-(x-mu)**2 / (2*sigma*sigma))

return m * n

def normal_cdf(x, mu, sigma):

# integral(-oo,x)

x = float(x)
mu = float(mu)
sigma = float(sigma)

# to standard form
x = (x - mu) / sigma

s = x
v = x
for i in range(1, 100):
    v = v * x * x / (2*i+1)
    s += v

return 0.5 + s/(2*math.pi)**0.5 * math.e ** (-x*x/2)

def difference(nbucket, nkey):

nbucket, nkey= int(nbucket), int(nkey)

# binomial distribution approximation by normal distribution
# find the bucket with minimal keys.
#
# the probability that a bucket has exactly i keys is:
#   # probability density function
#   normal_pdf(i, mu, sigma)
#
# the probability that a bucket has 0 ~ i keys is:
#   # cumulative distribution function
#   normal_cdf(i, mu, sigma)
#
# if the probability that a bucket has 0 ~ i keys is greater than 1/nbucket, we
# say there will be a bucket in hash table has:
# (i_0*p_0 + i_1*p_1 + ...)/(p_0 + p_1 + ..) keys.
p = 1.0 / nbucket
mu = nkey * p
sigma = math.sqrt(nkey * p * (1-p))

target = 1.0 / nbucket
minimal = mu
while True:

    xx = normal_cdf(minimal, mu, sigma)
    if abs(xx-target) < target/10:
        break

    minimal -= 1

return minimal, (mu-minimal) * 2 / (mu + (mu - minimal))

def difference_simulation(nbucket, nkey):

t = str(time.time())
nbucket, nkey= int(nbucket), int(nkey)

buckets = [0] * nbucket

for i in range(nkey):
    hsh = hashlib.sha1(t + str(i)).digest()
    buckets[hash(hsh) % nbucket] += 1

buckets.sort()
nmin, mmax = buckets[0], buckets[-1]

return nmin, float(mmax - nmin) / mmax

if name == "__main__":

nbucket, nkey= sys.argv[1:]

minimal, rate = difference(nbucket, nkey)
print 'by normal distribution:'
print '     min_bucket:', minimal
print '     difference:', rate

minimal, rate = difference_simulation(nbucket, nkey)
print 'by simulation:'
print '     min_bucket:', minimal
print '     difference:', rate
相關文章
相關標籤/搜索