Spark的OpenHashMap比jdk的HashMap快5倍,也会花费更少的空间。
protected var _keySet = new OpenHashSet[K](initialCapacity)
在OpenHashMap中,key将会存储在一个OpenHashSet中,因此先看到OpenHashSet的实现。
protected var _bitset = new BitSet(_capacity)
OpenHashSet的空间利用情况的存储的底层存储结构为BitSet,因此还需要进一步深入BitSet的实现。
private val words = new Array[Long](bit2words(numBits))
private def bit2words(numBits: Int) = ((numBits - 1) >> 6) + 1
BitSet在初始化的时候,会根据传入的初始化大小构造一个Long数组。其大小为OpenHashSet总空间-1与64相除并加1的结果。在OpenHashSet中,当要判断n位置上是否已经存在数据上时,将会通过BitSet来进行判断。
def get(index: Int): Boolean = {
val bitmask = 1L << (index & 0x3f) // mod 64 and shift
(words(index >> 6) & bitmask) != 0 // div by 64 and mask
}
Bitmask代表n与64取余的结果,这代表n位置具体细化到Long的数组中的元素时,其具体位置位于Long上的哪一位。之后相除64并与bitmask相与,如果为1则证明该位置有数据,否则代表该位置没数据。
这样可以看到,OpenHashSet通过位运算给出了一种简单又占用空间小的方式来快速判断是否某位置是否存在数据。
BitSet也提供了nextSetBit()方法来提供n位置下下一个key的存在位置的方法,这个方法主要用在获取迭代器的地方。
def nextSetBit(fromIndex: Int): Int = {
var wordIndex = fromIndex >> 6
if (wordIndex >= numWords) {
return -1
}
// Try to find the next set bit in the current word
val subIndex = fromIndex & 0x3f
var word = words(wordIndex) >> subIndex
if (word != 0) {
return (wordIndex << 6) + subIndex + java.lang.Long.numberOfTrailingZeros(word)
}
// Find the next set bit in the rest of the words
wordIndex += 1
while (wordIndex < numWords) {
word = words(wordIndex)
if (word != 0) {
return (wordIndex << 6) + java.lang.Long.numberOfTrailingZeros(word)
}
wordIndex += 1
}
-1
}
可以通过不断调用该方法,可以获取BitSet中所有存在数据的下标,达到迭代器的目的。
回到OpenHashSet,其具体的值存储形式为Array,没什么好说的。
_data = new Array[T](_capacity)
OpenHashSet的存入数据方法是addWithoutResize()方法。
def addWithoutResize(k: T): Int = {
var pos = hashcode(hasher.hash(k)) & _mask
var delta = 1
while (true) {
if (!_bitset.get(pos)) {
// This is a new key.
_data(pos) = k
_bitset.set(pos)
_size += 1
return pos | NONEXISTENCE_MASK
} else if (_data(pos) == k) {
// Found an existing key.
return pos
} else {
// quadratic probing with values increase by 1, 2, 3, ...
pos = (pos + delta) & _mask
delta += 1
}
}
throw new RuntimeException("Should never reach here.")
}
存数据先通过上文的BitSet来判断需要存入的hash位置是否有数据,没有直接存入,有的话,如果一样直接返回,达到去重的目的,否则加一定数量的delta继续上一层循环。
Delta初始量为1,随着碰撞的加大会逐渐增加数量,因此此处是解决碰撞的方案是平方探测法。同理,当需要获取某个值具体的位置的时候,则是重复步骤取值,直到位置上的值与要查询的值相等时,才是需要的位置。
OpenHashMap通过OpenHashSet来管理key的存放,而value的存储相对简单的多,仅仅只是一个普通的Array数组。
def update(k: K, v: V) {
if (k == null) {
haveNullValue = true
nullValue = v
} else {
val pos = _keySet.addWithoutResize(k) & OpenHashSet.POSITION_MASK
_values(pos) = v
_keySet.rehashIfNeeded(k, grow, move)
_oldValues = null
}
}
当要插入键值对的时候,将会根据上文OpenHashSet的addWithoutResize()方法直接插入key,key的位置也就是value的array数组的存放位置。在存入后,判断OpenHashSet是否需要扩容,扩容的同时也会构建一个新的大小的value数组,新数组的value下标同样会根据key在扩容后的坐标一致。
def computeNextPair(): (K, V) = {
if (pos == -1) { // Treat position -1 as looking at the null value
if (haveNullValue) {
pos += 1
return (null.asInstanceOf[K], nullValue)
}
pos += 1
}
pos = _keySet.nextPos(pos)
if (pos >= 0) {
val ret = (_keySet.getValue(pos), _values(pos))
pos += 1
ret
} else {
null
}
}
def hasNext: Boolean = nextPair != null
def next(): (K, V) = {
val pair = nextPair
nextPair = computeNextPair()
pair
}
此处为上文提到的迭代器实现,在next()方法可以通过BitSet的nextSetBit()方法直接简单的得到下一个存在数据的位置,达到迭代的目的。
总体来看,OpenHashMap可以通过BitSet快速定位数据并节约空间,采用平方探测法减少碰撞,实际存储空间只有实际大小两倍的Array数组。