前面簡單介紹了[ 經典的Times 33 哈希算法 ],這篇咱們經過分析Java 1.8 String類的哈希算法,繼續聊聊對乘數的選擇。java
/** Cache the hash code for the string */
private int hash;
/** Returns a hash code for this string. The hash code for a String object is computed as s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.) */
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
複製代碼
能夠看到,String的哈希算法也是採用了Times 33的思路,只不過乘數選擇了31。linux
其中算法
前一篇咱們提到:segmentfault
這個神奇的數字33,爲何用來計算哈希的效果會比其餘許多常數(不管是否爲質數)更有效,並無人給過足夠充分的解釋。所以,Ralf S. Engelschall嘗試經過本身的方法解釋其緣由。經過對1到256中的每一個數字進行測試,發現偶數的哈希效果很是差,根據用不了。而剩下的128個奇數,除了1以外,效果都差很少。這些奇數在分佈上都表現不錯,對哈希表的填充覆蓋大概在86%。緩存
從哈希效果來看(Chi^2應該是指卡方分佈),雖然33並不必定是最好的數值。但1七、3一、3三、6三、127和129等相對其餘的奇數的一個很明顯的優點是,因爲這些奇數與1六、3二、6四、128只相差1,能夠經過移位(如1 << 4 = 16)和加減1來代替乘法,速度更快。服務器
那麼接下來,咱們經過實驗數據,來看看偶數、奇數,以及1七、3一、3三、6三、127和129等這些神奇數字的哈希效果,來驗證Ralf S. Engelschall的說法。測試
我的筆記本,Windows 7操做系統,酷睿i5雙核64位CPU。優化
測試數據:CentOS Linux release 7.5.1804的/usr/share/dict/words字典文件對應的全部單詞。this
因爲CentOS上找不到該字典文件,經過yum -y install words進行了安裝。spa
/usr/share/dict/words共有479828個單詞,該文件連接的原始文件爲linux.words。
/** * 以1-256爲乘數,分別計算/usr/share/dict/words全部單詞的哈希衝突率、總耗時. * * @throws IOException */
@Test
public void testHash() throws IOException {
List<String> words = getWords();
System.out.println();
System.out.println("multiplier, conflictSize, conflictRate, timeCost, listSize, minHash, maxHash");
for (int i = 1; i <=256; i++) {
computeConflictRate(words, i);
}
}
/** * 讀取/usr/share/dict/words全部單詞 * * @return * @throws IOException */
private List<String> getWords() throws IOException {
// read file
InputStream is = HashConflictTester.class.getClassLoader().getResourceAsStream("linux.words");
List<String> lines = IOUtils.readLines(is, "UTF-8");
return lines;
}
/** * 計算衝突率 * * @param lines */
private void computeConflictRate(List<String> lines, int multiplier) {
// compute hash
long startTime = System.currentTimeMillis();
List<Integer> hashList = computeHashes(lines, multiplier);
long timeCost = System.currentTimeMillis() - startTime;
// find max and min hash
Comparator<Integer> comparator = (x,y) -> x > y ? 1 : (x < y ? -1 : 0);
int maxHash = hashList.parallelStream().max(comparator).get();
int minHash = hashList.parallelStream().min(comparator).get();
// hash set
Set<Integer> hashSet = hashList.parallelStream().collect(Collectors.toSet());
int conflictSize = lines.size() - hashSet.size();
float conflictRate = conflictSize * 1.0f / lines.size();
System.out.println(String.format("%s, %s, %s, %s, %s, %s, %s", multiplier, conflictSize, conflictRate, timeCost, lines.size(), minHash, maxHash));
}
/** * 根據乘數計算hash值 * * @param lines * @param multiplier * @return */
private List<Integer> computeHashes(List<String> lines, int multiplier) {
Function<String, Integer> hashFunction = x -> {
int hash = 0;
for (int i = 0; i < x.length(); i++) {
hash = (multiplier * hash) + x.charAt(i);
}
return hash;
};
return lines.parallelStream().map(hashFunction).collect(Collectors.toList());
}
複製代碼
執行測試方法testHash(),稍等片刻後,咱們將獲得一份測試報告。
經過對哈希衝突率進行降序排序,獲得下面的結果。
結果分析
咱們再對衝突數量爲1000之內的乘數進行分析,經過對執行耗時進行降序排序,獲得下面的結果。
分析1七、3一、3三、6三、127和129
整體上看,129執行耗時低,衝突率也是最小的,彷佛先擇它更爲合適?
將整個哈希空間[-2147483648,2147483647]分爲128個分區,分別統計每一個分區的哈希值數量,以此來觀察各個乘數的分佈狀況。每一個分區的哈希桶位爲2^32 / 128 = 33554432。
之因此經過分區來統計,主要是由於單詞數太多,嘗試過畫成圖表後密密麻麻的,沒法直觀的觀察對比。
@Test
public void testHashDistribution() throws IOException {
int[] multipliers = {2, 17, 31, 33, 63, 127, 73, 133, 237, 161};
List<String> words = getWords();
for (int multiplier : multipliers) {
List<Integer> hashList = computeHashes(words, multiplier);
Map<Integer, Integer> hashMap = partition(hashList);
System.out.println("\n" + multiplier + "\n,count");
hashMap.forEach((x, y) -> System.out.println(x + "," + y));
}
}
/** * 將整個哈希空間等分紅128份,統計每一個空間內的哈希值數量 * * @param hashs */
public static Map<Integer, Integer> partition(List<Integer> hashs) {
// step = 2^32 / 128 = 33554432
final int step = 33554432;
List<Integer> nums = new ArrayList<>();
Map<Integer, Integer> statistics = new LinkedHashMap<>();
int start = 0;
for (long i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i += step) {
final long min = i;
final long max = min + step;
int num = (int) hashs.parallelStream().filter(x -> x >= min && x < max).count();
statistics.put(start++, num);
nums.add(num);
}
// 爲了防止計算出錯,這裏驗證一下
int hashNum = nums.stream().reduce((x, y) -> x + y).get();
assert hashNum == hashs.size();
return statistics;
}
複製代碼
生成數據以後,保存文本爲csv後綴,經過Excel打開。再經過Excel的圖表功能,選擇柱狀圖,生成如下圖表。
乘數2
乘數17
乘數31
乘數33
乘數73
乘數127
乘數133
乘數161
乘數237
除了2和17,其餘數字的分佈基本都比較均勻。
如今咱們基本瞭解了Java String類的哈希乘數選擇31的緣由了,主要有如下幾點。
當參與哈希計算的項有不少個時,越大的乘數就越有可能出現結果溢出,從而丟失信息。我想這也是緣由之一吧。
儘管從測試結果來看,比3一、33大的奇數總體表現有更好的選擇。然而3一、33不只總體表現好,並且32的移位操做是最少的,理論上來說計算速度應該是最快的。
最後說明一下,我經過另外兩臺Linux服務器進行測試對比,發現結果基本一致。但以上測試方法不是很嚴謹,與實際生產運行可能存在誤差,結果僅供參考。
其中
stackoverflow.com/questions/2…
《Effective Java中文版本》第2版
更多文章,請關注公衆號:二進制之路