到目前為止,我們已經實現了三種快取方式:首先我們設法構建唯一字串,但是由於它的代價較高,於是我們使用了字首樹進行儲存;又由於字首樹在實際操作中所花的時間和空間都有不令人滿意之處,我們又引入了二叉搜尋樹。那麼二叉搜尋樹又有什麼缺點呢?其實前文已經談到過了,那就是從理論上來說,它的時間複雜度相對前兩個要高,在最壞情況下將會出現o(m * log(n))的時間複雜度——每次比較兩個字首樹需要耗費o(m),共比較o(log(n))次。
很顯然,與最理想的時間複雜度o(m)相比,其差距就在於n,也就是快取空間中已有的元素數量。如果元素越多,則n越大,log(n)也會隨之增大,則耗費o(m)的「次數」也就越多。換句話說,如果我們要改進效能,就要想辦法減少比較次數。乙個比較容易想到的做法便是對快取空間中的n個元素進行「分組」,在每次查詢時首先使用很小的時間複雜度,確定被查詢的表示式樹處於哪個組中,然後只需要與這個組中為數不多的幾個元素進行比較便可。這樣耗費o(m)的操作次數就少了,效能隨之提高。
既然要進行分組,那麼我們其實就是要從每個表示式樹中提取「特徵值」,再根據這個「特徵值」進行分組——不過這是有條件的,例如:
特徵值計算要盡可能的快,否則光計算一次特徵值就消耗大量時間,得不償失。
根據特徵值要能夠快速確定分組,原因如第1點。
特徵值要可以將元素盡可能地分散在不同組中,這樣每個組裡的元素會變得更少,更節省比較次數。
想到這裡,您應該已經得出結論了……這不就是雜湊值嗎?在.net framework中,乙個物件的雜湊值為乙個32位整型數值,然後便可以從乙個以int32型別為鍵的字典中快速地獲取乙個「分組」,這就已經滿足了上述第2點要求。因此,問題的關鍵在於如何「快速」地求出乙個表示式樹的雜湊值,並且要使這個雜湊值能夠盡可能地分布均勻。乙個表示式樹的雜湊值,顯然是由它內部的元素組成,因此我們只需要遍歷它的每個元素,將這些元素雜湊值結合為乙個單一的雜湊值即可——這是乙個o(m)的操作,非常高效。
為此,老趙實現了乙個expressionhasher類,用於計算乙個表示式樹的雜湊值,如下:
public classexpressionhasher : expressionvisitor
public int hashcode
protected virtual
expressionhasher hash(int value)
return this;
}protected virtual
expressionhasher hash(bool value)
return this;
}private static readonly object s_nullvalue = new object();
protected virtual
expressionhasher hash(object value)
return this;
}...
}
expressionhasher有個hash方法,可用於計算乙個表示式數的雜湊值。與之前的幾種expressionvisitor實現類似,expressionhasher也準備一些輔助方法供其他方法呼叫。這些輔助方法接受不同型別的引數,完全避免了資料的裝箱/拆箱,盡可能保持演算法的高效。從上面的**看出,我們不斷地向這些輔助方法內傳入物件時,它們會被累加到hashcode屬性中——這就是老趙在這裡使用的「組合方式」,將表示式樹中每個元素的雜湊值進行組合,最終成為整個表示式數的雜湊值。老趙無法證明這是一種優秀的雜湊組合演算法,但是從測試上來看,這麼做的效果還是不錯的(事實上,老趙隨機生成了大量表示式還沒有出現碰撞)。更關鍵的一點是,這麼做非常高效,如果將這些元素拼接起來,並得到最終字串的雜湊值可能會有更好的結果,但是其效能就比整數的相加要差許多了。
現在,我們只需要在visit每個節點的時候,把節點的屬性作為表示式樹的每個元素傳入對應的輔助方法便可,以下為部分**:
protected overrideexpression visit(expression exp)
protected override
expression visitbinary(binaryexpression b)
protected override
expression visitconstant(constantexpression c)
protected override
expression visitmemberaccess(memberexpression m)
protected override
expression visitmethodcall(methodcallexpression m)
...
按照我們剛才的設想,首先計算出乙個表示式樹的雜湊值,然後從字典中獲取具體的乙個分組,再從這個分組中進行查詢。使用這個方法則得到了hashedlistcache:
public classhashedlistcache
: iexpressioncache
where t : class
}finally
this.m_rwlock.enterwritelock();
tryif (!sortedlist.trygetvalue(key, out value))
return value;
}finally
}}
計算乙個表示式樹的雜湊值需要耗費o(m)的時間複雜度,從字典中查詢分組需要o(1),如果雜湊值夠好的話,每個分組中的表示式樹數量(k)應該非常少,這樣從中進行查詢的時間複雜度(o(log(k)))就非常接近於常數了。因此,hashedlistcache的查詢操作,其時間複雜度為o(m),這也達到了最為理想的時間複雜度。
到目前為止,我們為了解決表示式樹的快取問題,已經提出了4種不同的處理方式,並且編寫了多個操作表示式樹的輔助類:
******keybuilder:將表示式樹構造成唯一的字串。
prefixtreevisitor:根據表示式樹構造一顆字首樹。
expressioncomparer:比較兩個表示式樹的「大小」關係。
expressionhasher:計算乙個表示式樹的雜湊值。
回想起第一種做法,我們使用最原始的方式,使用字典來儲存物件,不過我們需要拼接出乙個龐大的字串,因為它具有「唯一性」。但是其實從那時開始,我們就已經走了一條彎路。在.net framework中,乙個物件如果要作為字典的「鍵」,難道一定要是字串嗎?很顯然,答案是否定的。事實上,任何型別的物件都可以作為字典的鍵,而字典認為兩個「鍵」物件相同依靠的是物件的gethashcode方法和equals方法。字典的整個查詢分兩步走:
首先根據gethashcode獲取物件雜湊值,用於確定需要查詢的物件在那個分組(或者說是「桶」,在資料結構中稱為雜湊表的「buckets」)中。
每個分組的物件數量很少,然後在使用equals方法依次進行比較,最終得到相同的那個值。
因為有了expressioncomparer和expressionhasher,我們已經可以非常輕鬆地實現那個作為「鍵」的物件了:
private classcachekey
public cachekey(expression exp)
private int m_hashcode;
private bool m_hashcodeinitialized = false;
public override int gethashcode()
return this.m_hashcode;
}public override bool equals(object obj)
}
最後再實現乙個dictionarycache:
public classdictionarycache
: iexpressioncache
where t : class
}finally
this.m_rwlock.enterwritelock();
tryvalue = creator(key);
this.m_storage.add(cachekey, value);
return value;
}finally
}}
dictionarycache的實現其實和hashedlistcache比較接近,不過從理論上說,dictionarycache的效能不如hashedlistcache。因為同樣在根據雜湊值獲取到分組後,dictionarycache中的分組元素數量可能會比hashedlistcache要多(因為字典中多個雜湊值也可以在同乙個分組中);同時,字典在同組的k個元素中找到指定元素使用o(k)的遍歷演算法,而二叉搜尋樹只要o(log(k))的時間複雜度——此消彼長,dictionarycache的效能自然就要略差一些了。
談表示式樹的快取(5) 引入雜湊值 1
到目前為止,我們已經實現了三種快取方式 首先我們設法構建唯一字串 但是由於它的代價較高,於是我們使用了字首樹 進行儲存 又由於字首樹在實際操作中所花的時間和空間都有不令人滿意之處,我們又引入了二叉搜尋樹 那麼二叉搜尋樹又有什麼缺點呢?其實前文已經談到過了,那就是從理論上來說,它的時間複雜度相對前兩個...
談表示式樹的快取(5) 引入雜湊值 2
回想起第一種做法,我們使用最原始的方式,使用字典來儲存物件,不過我們需要拼接出乙個龐大的字串,因為它具有 唯一性 但是其實從那時開始,我們就已經走了一條彎路。在.net framework中,乙個物件如果要作為字典的 鍵 難道一定要是字串嗎?很顯然,答案是否定的。事實上,任何型別的物件都可以作為字典...
談表示式樹的快取(1) 引言
表示式樹 expression tree 是.net 3.5中引入的一種表達方式。表示式樹的運用十分廣泛,可以直觀地表現出各種 資料 甚至 邏輯 和 行為 再者,表示式樹是強型別的,因此合理地使用這個新特性可以讓 編寫變得優雅,方便。乙個最簡單而常見的例子便是,某些朋友目前就已經喜歡使用表示式樹來代...