日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

用了十年竟然都不对,Java、Rust、Go主流编程语言的哈希表比较

發布時間:2023/12/3 java 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 用了十年竟然都不对,Java、Rust、Go主流编程语言的哈希表比较 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

哈希表(HashMap、字典是日常編程當中所經常用到的一種數據結構,程序員經常接解到的大數據Hadoop技術棧、Redis緩存數據庫等等最近熱度很高的技術,其實都是對鍵值(key-value)數據的高效存儲與提取,而key-value恰恰就是哈希表中存儲的元素結構,可以說Redis、HDFS這些都是哈希表的經典應用,不過筆者之前也只知道哈希表比較快,但對于具體什么場景下快,怎么用才快等等知識卻一知半解因此這里把目前的一些研究成果分享給大家。

重新認識哈希表

所謂的哈希表就是通過哈希算法快速搜索查詢元素的方法,比如說你要在茫茫人海當中找到一位筆名叫做beyondma的博主,但卻并不知道他具體博客地址,在這種情況下就只能在所有的博主范圍內展開逐個的排查與摸索,運氣差的話我可能以找遍所有n個博主的主頁,才到beyondma,這也就是這種遍歷查找的時間復雜度是o(n),查找的時間會隨著博主的數量而線增長。

而哈希算法就是直接將beyondma這個名字進行算法處理,直接得到beyondma的博客地址信息,在哈希算法的加持下定位某一元素的時間度變成了o(1),由于哈希算法能夠將key(鍵值本例中指beyond)和value(本例中指beyond.csdn.net)以o(1)的時間復雜度,直接對應起來,因此哈希表被人稱為key-value表,存儲的元素也被稱為key-value對(鍵值對)。哈希表的查找過程特別像查字典,給出一個字并找到這個字在字典中的位置,只是哈希表在一般情況下都很快。當然哈希表也有代價:

空間換時間:哈希算法也稱為散列算法,這種叫法相對比較直觀,由于哈希算法是通過計算確認存儲地址的,因此首先進入到哈希表的元素并不一定存到第一個位置,存儲n個鍵值對的哈希表往往會消耗比切片多很多的內存空間。

哈希碰撞:哈希碰撞是指不同的值,經過哈希計算后得到的內存地址槽位是相同的,也就是說相同的地址上要存儲兩個以上的鍵值對,一旦發生這種情況也就是哈希碰撞了發生碰撞的場景下哈希表會進行退化其中Java會在碰撞強度到達一定級別后,使用紅黑樹的方式來進行哈希鍵值對的存儲,而Go和Rust一般都是退化成為鏈表。

下面我們首先來詳細講兩個哈希常見誤用。

哈希表的誤用

不要遍歷哈希表!:局部快,不意味著整體快,由于哈希表提取單個元素的速度很快,因此整個遍歷整個集合所需要的時間也會更短,這種看法明顯是個美麗的誤會。

我們后文也會具體講到,哈希表在遍歷方面的表現結果,是由計算機組成原理決定的,與Go、Rust和Java的區別不大,因此以下例子先以Go語言的代碼為例來說明。

package?mainimport?("fmt""time")func?main()?{testmap?:=?make(map[int]int)len?:=?1000000//tests1ice?:=?make([]int,?len,?len)for?i?:=?0;?i?<?len;?i++?{testmap[i]?=?i?+?1}sum?:=?0now?:=?time.Now().UnixNano()for?k,?v?:=?range?testmap?{sum?=?sum?+?k?+?v}diff?:=?time.Now().UnixNano()?-?nowfmt.Println("sum=",?sum)fmt.Println("diff=",?diff)//?fmt?PrintIn("slice=",?slice)}

可以看到使用哈希表進行遍歷的話,以上代碼運行的結果為:

sum= 1000000000000diff= 29297200成功: 進程退出代碼 0.

而對比使用切片遍歷的代碼如下:

package?mainimport?("fmt""time")func?main()?{//testmap?:=?make(map[int]int)len?:=?1000000tests1ice?:=?make([]int,?len,?len)for?i?:=?0;?i?<?len;?i++?{tests1ice[i]?=?i?+?1}sum?:=?0now?:=?time.Now().UnixNano()for?k,?v?:=?range?tests1ice?{sum?=?sum?+?k?+?v}diff?:=?time.Now().UnixNano()?-?nowfmt.Println("sum=",?sum)fmt.Println("diff=",?diff)//?fmt?PrintIn("slice=",?slice)}

以上代碼運行結果為:

sum= 1000000000000diff= 1953900成功: 進程退出代碼 0.

可以看到同樣長度的集合遍歷性能表現,切片的耗時只有哈希表的5%左右,兩者幾乎相差兩個數量級。

數據訪問局部性原理的制約局部性原理可能是計算機基本原理中威力最強的基本定理之一,也是程序員在編程過程中必須要考慮的規律,因此我們看到在計算機世界中局部性原理,經常在速度不匹配的存儲介質中得到運用,比如英特爾的CPU往往分為三級高速緩存,彼此之間的速度差距大概在8到10倍之間,其中高速緩存中的第三級緩存又比內存快10倍,這樣彼此之間各差10倍左右的緩存體系加速效果最好,這就像軍事行動中,先鋒部隊既要率先行動,又不能與大部隊過于脫節,才能圓滿的完成任務。在實際CPU的工作當中,如果數據單元A1被訪問了,那么A1的鄰居A0和A2被訪問到的可能性也會極大的增加,因此CPU一般都會在數據單元A1被訪問的同時,將他的鄰居們調入高速緩存。

也就是說切片這種在內存當中連續分布的數據結構,其元素都是以高速緩存行的大小為單位讀入到高速緩存的,而高速緩存的平均速度又是內存的幾十倍,因此相當于一次讀取操作,就能快速處理好幾個元素;但由于哈希表實際也是稀疏表,一個鍵值對的周圍可能沒有其它有效鍵值對,因此哈希表在遍歷時實際上只能一個一個元素的處理。這樣比較下來哈希表在單個元素的訪問上快,但在整體遍歷上慢也就不足為奇了。

在元素不多不要用哈希表我經常看到有不少程序員在元素不多的情況下,還堅持使用哈希表來建立key-value對應關系,其實這樣的做法并不會帶來效率的提升,正如我們剛剛所說,哈希算法也被稱為散列算法,鍵值對的內存地址分布很可能并不連續,這就特別不方便局部性原理發揮作用。剛剛我們上文也提到了內存緩存行的大小通常是64byte在實際測試過程中可以看到如果元素能在個內存緩存行存儲下來就不要用哈希表了,這時候用數據切片,每次遍歷查找的性能反而比哈希表更快。具體代碼如下:

哈希表實現示例

package?mainimport?("fmt""time")func?main()?{testmap?:=?make(map[int]int)len?:=?10times?:=?100000//tests1ice?:=?make([]int,?len,?len)for?i?:=?0;?i?<?len;?i++?{testmap[i]?=?i?+?1}sum?:=?0now?:=?time.Now().UnixNano()for?i?:=?0;?i?<?times;?i++?{//for?k,?v?:=?range?testmap?{//if?i%len?==?v?{sum?=?sum?+?i%len?+?testmap[i%len]//break//}//}//sum?=?sum?+?k?+?v//tests1ice[i%len]?=?i?+?1}diff?:=?time.Now().UnixNano()?-?nowfmt.Println("sum=",?sum)fmt.Println("diff=",?diff)//?fmt?PrintIn("slice=",?slice)}

以上代碼結果如下:

sum= 1000000diff= 2929500

而切片遍歷查找的實現如下:

package?mainimport?("fmt""time")func?main()?{//testmap?:=?make(map[int]int)len?:=?10times?:=?100000tests1ice?:=?make([]int,?len,?len)for?i?:=?0;?i?<?len;?i++?{tests1ice[i]?=?i?+?1}sum?:=?0now?:=?time.Now().UnixNano()for?i?:=?0;?i?<?times;?i++?{for?k,?v?:=?range?tests1ice?{if?i%len?==?k?{sum?=?sum?+?k?+?vbreak}}//sum?=?sum?+?k?+?v//tests1ice[i%len]?=?i?+?1}diff?:=?time.Now().UnixNano()?-?nowfmt.Println("sum=",?sum)fmt.Println("diff=",?diff)//?fmt?PrintIn("slice=",?slice)}

sum= 810000diff= 1953000成功: 進程退出代碼 0.

少元素方面集合的元素定位性能上,哈希表比切片慢了40%,當然這也是局部性原理造成的,由于元素比較少,因此切片這樣內存連續數據結構,完全可以在高速緩存中完成數據的查找定位,這樣綜合下來其性能反而還要比哈希表要快。

正如前文所述,哈希算法的工作機制本身就決定了哈希表對存儲空間就有一定的浪費,因此在沒有性能優勢的情況下,尤其是上述遍歷及短表的場景下,就不要再用哈希表了,完全沒有必要。

哈希表的實現機制要點

在筆者看了部分哈希表的代碼之后,Java、Go和Rust這三種語言有一些相同的機制,也有一些不同,其中有兩點值得關注,當然由于水平有限,如有錯誤之處敬請指正。

避免使用連續內存塊:我們知道在內存、硬盤等存儲設備的管理中,連續的空間往往是比較寶貴的,而哈希表是相對比較稀疏的數據結構,因此Java、Go和Rust基本都引用了一些比如桶的機制,盡量避免占用連續的內存塊。以Go語言的實現為例:

type hmap struct {count ????int // map的長度flags ????uint8B ????????uint8 ?// map中的bucket的數量,noverflow uint16 //hash0 ????uint32 // hash 種子buckets ???unsafe.Pointer // 指向桶的指針oldbuckets unsafe.Pointer // 指向舊桶的指針,這里用于溢出nevacuate ?uintptr ???????extra *mapextra // optional fields}// 在桶溢出的時候會用到extratype mapextra struct {overflow ???*[]*bmapoldoverflow *[]*bmapnextOverflow *bmap}type bmap struct {tophash [bucketCnt]uint8// Map中的哈希值的高8位為桶的地址}

在訪問Map中的鍵值對時,需要先計算key的哈希值,其中哈希的值的低8位定位到具體的桶(bucket),通過高8位在桶內定位到具體的位置而不同桶之間所占用的內存區域也不需要是連續的空間,這樣也就從一定程度上彌補哈希表占用空間較大的缺點。

哈希碰撞處理:我們剛剛也介紹了哈希表碰撞的內容,也就是出現了不同的鍵值對要存儲在同一個內存槽位的場景,極端情況下是所有鍵值對全部發生碰撞,這樣哈希表實際也就退化成了鏈表,Java對碰撞的處理相對比較成熟,如果退化的鏈表長度大于8,那么Java會選擇用紅黑樹這種近似于二叉排序樹的數據結構進行替代,從而保證定位性能不低于O(logn)

?

而如果鏈表的長度小于等于8,那么如我們上文介紹,在數據長度比較短的情況下其實鏈表的性能可能還會更好,沒必要使用引入紅黑樹,由此可見Java這門語言的確已經非常成熟。

總結

以上是生活随笔為你收集整理的用了十年竟然都不对,Java、Rust、Go主流编程语言的哈希表比较的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。