爲何Java String哈希乘數爲31?

前面簡單介紹了[ 經典的Times 33 哈希算法 ],這篇咱們經過分析Java 1.8 String類的哈希算法,繼續聊聊對乘數的選擇。java

String類的hashCode()源碼

/** 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

其中算法

  • hash默認值爲0.
  • 判斷h == 0是爲了緩存哈希值.
  • 判斷value.length > 0是由於空字符串的哈希值爲0.

用數聽說話

前一篇咱們提到: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(),稍等片刻後,咱們將獲得一份測試報告。

哈希衝突率降序排序

經過對哈希衝突率進行降序排序,獲得下面的結果。

結果分析

  • 偶數的衝突率基本都很高,只有少數例外。
  • 較小的乘數,衝突率也比較高,如1至20。
  • 乘數一、二、256的分佈不均勻。Java哈希值爲32位int類型,取值範圍爲[-2147483648,2147483647]。

哈希衝突率降序排序

哈希耗時降序排序

咱們再對衝突數量爲1000之內的乘數進行分析,經過對執行耗時進行降序排序,獲得下面的結果。

分析1七、3一、3三、6三、127和129

  • 17在上一輪已經出局。
  • 63執行計算耗時比較長。
  • 3一、33的衝突率分別爲0.13%、0.14%,執行耗時分別爲十、11,實時基本至關
  • 12七、129的衝突率分別爲0.01%、0.004%,執行耗時分別爲九、10

整體上看,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

乘數2

乘數17

乘數17

乘數31

乘數31

乘數33

乘數33

乘數73

乘數73

乘數127

乘數127

乘數133

乘數133

乘數161

乘數161

乘數237

乘數237

除了2和17,其餘數字的分佈基本都比較均勻。

總結

如今咱們基本瞭解了Java String類的哈希乘數選擇31的緣由了,主要有如下幾點。

  • 31是奇素數。
  • 哈希分佈較爲均勻。偶數的衝突率基本都很高,只有少數例外。較小的乘數,衝突率也比較高,如1至20
  • 哈希計算速度快。可用移位和減法來代替乘法。現代的VM能夠自動完成這種優化,如31 * i = (i << 5) - i
  • 31和33的計算速度和哈希分佈基本一致,總體表現好,選擇它們就很天然了。

當參與哈希計算的項有不少個時,越大的乘數就越有可能出現結果溢出,從而丟失信息。我想這也是緣由之一吧。

儘管從測試結果來看,比3一、33大的奇數總體表現有更好的選擇。然而3一、33不只總體表現好,並且32的移位操做是最少的,理論上來說計算速度應該是最快的。

最後說明一下,我經過另外兩臺Linux服務器進行測試對比,發現結果基本一致。但以上測試方法不是很嚴謹,與實際生產運行可能存在誤差,結果僅供參考。

幾個經常使用實現選項

values chosen to initialize h and a for some of the popular implementations

其中

  • INITIAL_VALUE:哈希初始值。Java String的初始值hash=0。
  • a:哈希乘數。Java String的哈希乘數爲31。

參考

stackoverflow.com/questions/2…

segmentfault.com/a/119000001…

en.wikipedia.org/wiki/Univer…

《Effective Java中文版本》第2版

我的公衆號

更多文章,請關注公衆號:二進制之路

二進制之路
相關文章
相關標籤/搜索