Spark Shuffle源码分析系列之AppendOnlyMap

AppendOnlyMap是spark自己实现的HashMap,与java自身的HashMap不同,它只能添加数据,不能remove,同时支持在内存中对任务执行结果进行聚合运算,本节我们来看下这个数据结构。

整体结构

AppendOnlyMap本质是一个HashMap,内部使用数组来存储数据,它的key-value是使用数组中相邻的两个元素进行存储的,如下图所示:
在这里插入图片描述

我们来看下源码中相关存储以及容量的定义:

val MAXIMUM_CAPACITY = (1 << 29)
private var capacity = nextPowerOf2(initialCapacity)
private var data = new Array[AnyRef](2 * capacity)
// 计算数据存放位置的掩码。计算mask的表达式为capacity – 1
private var mask = capacity - 1
// 用于计算data数组容量增长的阈值的负载因子。固定为0.7。
private val LOAD_FACTOR = 0.7
// data数组容量增长的阈值。
private var growThreshold = (LOAD_FACTOR * capacity).toInt

private def nextPowerOf2(n: Int): Int = {
   
  // 获取n的中最高的一位1所代表的数字
  val highBit = Integer.highestOneBit(n)
  // 如果与n相同,则取n,否则左移1位,即乘以2
  if (highBit == n) n else highBit << 1
}

可以看出来数组的容量是2的倍数,而且最大容量capacity1 << 29,另外定义了容量增长的负载因子,固定为0.7,这是为什么呢?

  1. 容量是2的倍数:我们知道计算机中&运算速度快比%取模运算速度要快,总容量是2的倍数时候,我们拿到key时候,对齐进行hash运算得到hash值,我们能保证元素最终的索引值肯定在capacity中,不会超出数组长度,所以可以通过执行hash % n获取索引位置,如果是2的倍数,那么取模运算可以使用(n - 1) & hash来替代,从而可以加速索引计算的速度,源码中定义了掩码为capacity - 1,只需要跟hash &运算即可得到相应的index位置;
  2. 最大容量1<<29:内部数据存储是使用Array,最大容量是Int,所以最大值是2^31 - 1,又必须是2的倍数,所以最大值是2^30,这里定义最大capacity1 << 29,再乘以2<key-value各使用一个index>即为2^30
  3. 负载因子是0.7:负载因子和当前容量大小的乘积决定了数组扩容的阈值,当负载因子较大时,数组扩容的可能性就会少,所以相对占用内存较少,但是hash冲突会相对较多,查询的时间也会增长;负载因子较少的时候,给数组扩容的可能性就高,那么内存空间占用就多,但是hash冲突相对较少,查出的时间也会减少。0.7默认值是时间和空间上的一种折中的说法。

Hash冲突

当添加元素时候,需要计算key对应的hash值,然后寻找在capacity内的下标值,这时候可能会遇到改下标位置处已经被填充了的情况,发生了冲突,Java原生的HashMap是通过拉链法去解决hash冲突的,AppendOnlyMap是通过开放地址法–线性探测的方法进行解决冲突的,线性探测间隔总是固定的,通常为1.

H+1, H+2, H+3, H+4, …, H+k,也就是当地址冲突,不断对地址+1,不断探测进行找到没有冲突的地方即可。

扩容

当当前数组的存储元素达到capacity * LOAD_FACTOR时候就要进行扩容操作,扩容大小设置为之前元素的2倍,然后经过一下步骤处理:

  1. 新建一个二倍容量的数组,并计算新的掩码;
  2. 对于老的数组中的每个元素,依次遍历,重新hash;
  3. 如果计算的位置相同,即hash冲突,进行二次探测,那么就找下一个位置<第一次加1,第二次加2,第三次加3>,以此类推;
  4. 替换数组,更新新的容量大小,掩码以及增长阈值。
protected def growTable() {
   
  // 扩容的新容量是当前容量的2倍
  val newCapacity = capacity * 2
  // 新容量不可超过MAXIMUM_CAPACITY,即1 << 29
  require(newCapacity <= MAXIMUM_CAPACITY, s"Can't contain more than ${growThreshold} elements")

  // 创建一个两倍于当前容量的数组
  val newData = new Array[AnyRef](2 * newCapacity)
  // 计算新数组的掩码
  val newMask = newCapacity - 1
  var oldPos = 0

  // 将老数组中的元素拷贝到新数组的指定索引位置
  while (oldPos < capacity) {
   
    if (!data(2 * oldPos).eq(null)) {
   
      // 取得原位置上的键和值
      val key = data(2 * oldPos)
      val value = data(2 * oldPos + 1)
      // 计算存放对应键的新索引位置,使用新的mask掩码进行rehash计算
      var newPos = rehash(key.hashCode) & newMask
      var i = 1
      var keepGoing = true
      while (keepGoing) {
   
        // 检查新索引上是否存在键
        val curKey = newData(2 * newPos)
        if (curKey.eq(null)) {
    // 对应新索引位置上的键为null
          // 更新键和值
          newData(2 * newPos) = key
          newData(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值