Map中的Hash问题

今天是祖国70大庆,没有出游而是选择了工作。因为这个里面涉及到一个接下来工作的核心,用Int替代String作为Map的key。我必须要充分的论证。

关于Hash的问题一直萦绕在我心头。在数据库计算,所有关乎Map的计算都会是一个耗时的计算,比如分组比如HashJoin等。因此可以肯定得说想加快计算速度,首先得搞定Map。所有Map中理论速度最快必然是Hash Map,get操作时间为产量时间。所以Hash Map必然是首选。

Hash Map分组查询

这里不深究其实现,只关注于其使用。

Equals

对于分组汇总来说,以String类型的作为Key是再正常不过了,例如:group by “省份”,”城市”。 也就是说这里的Key是”省份”,”城市”的一个组合Key。一般就会用一个String数组来存储省份和城市的值,如果select的是sum(“GDP”),那么可以简化用Map[String[],Int]这样结构类存储结果。我简单模拟了一下,结果如下

group size:59049
time:10074ms
hash:10000000
equals:9881672

分组数是6w,也就是有6w个key值。hash调用了1kw次,equals方法调用了1kw。这其实是非常糟糕的,因为equals方法调用太多了,说明有冲突。

下面我们将”省份”,”城市”替换为”省份ID”,”城市ID”,也就是group by “省份ID”,”城市ID”。再看一下最终结果

group size:59049
time:1724ms
hash:10000000
equals: 9941468

分组数与equals方法调用次数都比较相似,但是时间损耗却相差甚远。

再看一组数据,依然是用String和Int作为key。

String作为Key值的情况

group size:10000000
time:910ms
hash:10000000
equals: 11429

Int作为Key值的情况

group size:10000000
time:843ms
hash:10000000
equals: 11791

第一个例子String主要慢的原因是其equals速度远慢于int类型。因为存在大量的Hash冲突才导致这么多的equals调用。第二个列子中,我扩大了Key的范围,避免过多了的冲突,于是String的速度立刻降了下来。 为了更为直接的来比较int和string数组的equals差距,增加了一个针对equals的beachmark代码。

for (int i = 0; i < rowSize; i++) {
	for (int j = 0; j < innerSize; j++) {
		if (objectGroupKeys[index[i]].equals(objectGroupKeys[innerIndex[j]])) {
            find++;
       }
	}
}

其中indexinnerIndex都是为随机数,避免CPU缓存导致结果不准确。分别针对string和int得到以下数据

String的equals结果:
group size:48324
time:42174ms
find:27336
Int的equals结果:
group size:48243
time:13418ms
find:26732

时间上基本与分组计算中的比例相符,因此在相同的冲突比例下int数组要远优于String数组。

此外String还有个问题,String数组越长速度越慢,这和String的equals实现有关。下面的数据是在相同场景下,仅增加了String的key值长度。

String的equals结果:
group size:48280
time:145041ms
find:26812

Hash Code

通常情况下对map进行get或者put操作,key值的hashcode只会获取一次。如果key值是临时生成的,则没有必要缓存hash code值,但是如果key是反复利用的而且code对象比较复杂,计算相对耗时的话,将hashcode缓存下来是一个很好的优化。

冲突问题

如果Hash Map发生了验证的冲突,那么会是一件非常可怕的事情,Map的性能会急剧下降。来看一个数据,依然使用上面的场景中的代码,但是将int改为了Double对象。

group size:59049
time: 45143 ms
hash:10000000
equals: 1851012879

发现速度奇慢无比,根本原因是有大量的equals的调用,典型的hash发生了严重冲突。我将hash结果统计了一下,发现6w分组竟然只有1k不到的hash值。从equals次数上来看,可以看出平均一次get需要20次的equals

这里的冲突和Double对象的hashcode算法有关。通过简单的计算可以得到[0.0, 0.0]和[2.0,2.0]的hashcode是相同的,而模拟数据中的多数这样的double数据,于是发生了验证的冲突。

因此从上面可以看出,尽量避免使用String作为Key,优先使用Int。重写hashcode方法避免冲突。

##对付冲突 最理想的情况是没有冲突,用hashcode去取数总是唯一值。但是实际情况很难做到这样。首先空间不准许,其次hashcode是int类型,其长度也不准许。那么该如何避免激烈冲突呢.TO BE CONTINUE

更早的文章

Spark BroadcastHashJoin Timeout异常简单分析笔记

异常信息如下java.util.concurrent.TimeoutException: Futures timed out after [300 seconds] at scala.concurrent.impl.Promise$DefaultPromise.ready(Promise.scala:219) at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:223) at org.apache.spa...…

继续阅读