coding4fun 詞頻統計的優化思路

在此次的coding4fun活動中已經有不少同窗分享了精彩的優化思路。個人思路其實大同小異,下面就挑一些於衆不一樣的地方分享吧:數組

第一個不一樣點:數據結構

在結構上選擇了簡化版的Trie做爲查找結構。簡化版Trie的結構就是一顆n叉樹,每一個節點對應一個狀態。選擇簡化版Trie的緣由是它的樹狀結構很容易用CAS實現無鎖並行,而相比hashtable沒有hash衝突和rehash的問題,相比複雜Trie結構如Double-ArrayTrie又比較容易實現:併發

215212914.jpg

簡化版Trie的一個重要參數就是樹的寬度(W),對應每一層有多少個子節點,它等於存放子節點的數組大小。若是W越大,每一個節點佔據的內存就越大,節點利用率就越低。Trie的每一層能夠有不一樣的W,在極限外推下,若是Trie樹的第一層W很是大,保證絕大部分節點在第一層能放下,這個內存結構就與hashtable沒有太大區別了:215305316.jpgide

對Trie的調優集中在節點利用率上。若是節點利用率越高,相同單詞量的狀況下數據結構佔據的內存就越小(假設內存能徹底放下的話),CPU隨機訪問這些節點的cachehit就會越高。——短期內無法對樹的查找複雜度O(logn)進行改造,因此只能設法下降數據結構的內存佔用(工做集)。性能

Trie樹的查找是先把輸入拆分紅一系列「狀態」序列,而後根據這組狀態序列在用「樹」表示的狀態遷移有向圖中定位最終的節點。在簡化的Trie結構中,「狀態」的總數就決定了Trie樹的寬度W。所以怎樣把輸入有效的拆分紅「狀態」或者說定位子節點index是一樣重要的,例如輸入字符串‘ABCDEFG’:測試

  1. 若是按原始char內碼拆分,生成的子節點index序列是:{65,66,67,68,69,70,71},須要一棵W=256的Trie樹存放;優化

  2. 若是隻考慮字母,忽略大小寫壓縮一下,生成的子節點index序列是:{0,1,2,3,4,5,6},只須要一棵W=26的Trie樹就能放下;線程

  3. 若是在上一條的基礎下,按Bit逐位拆分每一個字母,則產生的index序列是:{0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,0,1,0,0,0,1,1,0,0},只須要W=2的Trie(它實際上是棵二叉樹),可是查找路徑會增長不少。blog

BTW其實這就是BitwiseTrie,不過第一Java下不能利用CPU指令優化位運算,第二BitwiseTrie更適合定長輸入,因此測試性能並不太友好。排序

  1. 更加靈活(複雜)的拆分辦法,在上述按Bit拆分的基礎上,再合併相鄰Bit增長寬度,下降查找路徑。例如合併3位:{0,4,0,4,0,3,0,2,2,1,6,0},須要一個W=8的Trie存儲。這個方法可讓任意W值的Trie樹接受任意的狀態序列輸入。

爲了方便對不一樣W和結構的Trie性能進行測試,我定義了一組Tries/Sequencer接口,由Tries接口維護「狀態」數據結構,Sequencer產生「狀態」的index序列。在所有的測試中我嘗試了若干種不一樣的Tries和Sequencer實現,最後發現按照方式2來最大限度的壓縮index序列的AlphabetSequencer與最簡的SimpleTries/ConcurrentTries(使用CAS並行插入節點)實際性能最好。這一塊就很少介紹了,你們有興趣能夠閱讀Gitlab的代碼。

第二個不一樣點:

與大部分同窗利用內存映射文件一次性把文件讀到內存不一樣,我在一開始就直接把文件平均分紅若干個分區,讓每一個工做線程單獨掃描一個分區進行分詞,這樣能夠實現徹底並行:

215354297.jpg

由於平均分區這個辦法太簡單,可能會出現尾部「半個單詞」的問題,漏掉分區結尾的單詞。解決尾部半個單詞的辦法很簡單,寫成僞代碼是這樣的:

If(不是從文件開頭讀取){

忽略分區的第一個單詞;

}

讀取和計算分區中的單詞;

If(沒有讀到文件末尾){

繼續向後多讀一個單詞,直到單詞結束;

}

具體的併發執行過程以下圖。其中併發加載是最慢的,消耗通常在3s以上,而併發排序通常是6x-8x毫秒:220256704.jpg

實際調優中發現,儘管多個線程併發訪問相同的數據結構,可是由於單詞總量不大(3w多),Trie樹的絕大部分節點都是在運行的最初階段插入的,CAS衝突消耗的時間不多(100-200ms級別),主要的消耗仍是在樹查找與內存訪問。

相關文章
相關標籤/搜索