【Go】map 源码解析 - group

标题

  • 前言
  • [1. map 结构](#1. map 结构)
  • [2. Group 结构](#2. Group 结构)
    • [2.1 字段定义](#2.1 字段定义)
    • [2.2 相关方法](#2.2 相关方法)
      • [2.2.1 bitset](#2.2.1 bitset)
      • [2.2.2 ctrlGroup](#2.2.2 ctrlGroup)
      • [2.2.3 groupReference](#2.2.3 groupReference)
      • [2.2.4 groupsReference](#2.2.4 groupsReference)
  • [3. 小结](#3. 小结)

前言

目前正在学习 Go 相关数据结构的源码。之前研究 Java 时,第一个深入分析的源码就是 HashMap,因此这次学习 Go 也以 map 为起点,下面是之前 Java 的 HashMap 介绍。- 第一篇文章:源码解析系列:HashMap(1)

下面是 Go 的 map 介绍。

1. map 结构

上面是 Go 的简单示例图,最顶层的就是 Map,Map 下面有两个分支:

  1. Small Map :如果用一个 group 就能装下所有 k-v,那么就不需要多个 group,此时 map 类型就是 small map,检索速度更快。
  2. Normal Map :如果一个 map 里的 k-v 数量超过 8 个,就需要多个 group,此时 map 结构为 Map -> Directory -> Group。可以把 Directory 理解为书页,查询时需要先计算出当前 key 属于哪一页(哪个 group),再进入该 group 内查询。

2. Group 结构

上面介绍了 Map 的整体结构,下面来详细说明 group 的结构,group 的源码路径在 internal\runtime\maps\group.go。

2.1 字段定义

group 的内存结构如下:[ ctrls 控制字 ][ slot0 ][ slot1 ][ slot2 ]...,其中 ctrls 控制字 是 int64 类型的,8 个字节,对应 8 个 slot,也就是说一个 group 里面有 8 个 slot。slot 就是一个 Elem,也就是存储 key-value,但是要注意 go 里面并没有明确声明 slot 这个结构体,而是通过地址访问。实际上 group 的整体结构可以看成下面的结构体。

go 复制代码
// type group struct {
//     ctrls ctrlGroup
//     slots [abi.MapGroupSlots]slot
// }
//
// type slot struct {
//     key  typ.Key
//     elem typ.Elem
// }

ctrls 里面每一个控制字节都可以用 ctrl 来表示,在源码中的定义如下。

go 复制代码
// 哈希表中的每个 Slot 都有一个控制字节,用于表示 Slot 的状态。
// 控制字节有三种状态:空、已删除和已占用,编码如下:
//
//	  空:1 0 0 0 0 0 0 0
//	已删除:1 1 1 1 1 1 1 0
//	已占用:0 h h h h h h h  // h 表示哈希值的 H2 部分
//
// TODO(prattmic):考虑将最高位取反,使零值可以表示空状态。
type ctrl uint8

// ctrlGroup 表示一个 Group 中的固定数量控制字节,
// 并将这些控制字节整体存储在一个 uint64 中。
type ctrlGroup uint64

其中 ctrl 代表一个 slot 的状态,group 中每一个 slot 都有三种状态:

  • ctrlEmpty: 0b10000000,代表这个 slot 是空的,可以往里面写入 key-value。
  • ctrlDeleted: 0b11111110,代表这个 slot 原来里面有 key-value,但是被删除了,因此将这个 slot 标记为 已删除。
  • ctrlFull: 源码里面没有定义这个状态,如果 slot 里面写入了 key-value,那么这个 slot 的状态会被标记成 0b 0hhhhhhh,其中后面的 7 个 h 就是 key 的 h2 部分。

根据上面三种状态就能简单判断出一个 slot 到底是 empty、deleted 还是 full,从而判断这个 slot 是否能继续写入,这个是后面的逻辑了。

当然,一个 ctrlGroup 是 64 位,在遍历一个 group 查询里面的 key-value,如何快速知道这个 group 里面哪些 slot 里面是满足条件的,源码中就提供了 type bitset uint64,当然这个字段不是用来存当前有哪些 slot 是空的,哪些 slot 是满的。当调用 matchEmpty、matchEmptyOrDeleted 的时候会先根据 group 里面的 slot 内容生成一个 bitset,然后遍历这个 bitset 来遍历 slot。

go 复制代码
// bitset 表示 Group 中一组 Slot 的集合,用来简单记录下 slot 的状态,比如简单
// 记录下这个 slot 上面有没有数据,这样可以快速返回有哪些 slot 上面有 key-value
type bitset uint64

在不同架构下,这个 bitset 的逻辑不同:

  • 在 AMD64 架构上,bitset 为每个 slot 只使用 1 位,也就是总共 8 bit,此时 bitset 只用最低 8 位记录,高 56 位没用,最终结果就是 0b00100101。
  • 在其他通用结构中,每个 slot 用一个字节(8 bit),否则为 0x00。

所以为了兼容这两种情况,使用了 uint64 来兼容上面这两种情况,那 为什么 AMD64 只用 8 bit? AMD64 上有专门的 SIMD / 位操作指令,可以一次比较 8 个控制字节,并直接生成一个 匹配掩码 :

go 复制代码
8 个控制字节:  [C0] [C1] [C2] [C3] [C4] [C5] [C6] [C7]
比较后直接得到:[ 1 ] [ 0 ] [ 1 ] [ 0 ] [ 0 ] [ 1 ] [ 0 ] [ 0 ]

AMD64 这种计算方式也比较方便,压缩成 1 个字节,相当于直接操作二进制,

2.2 相关方法

下面是 group.go 里面针对上面几个状态获取的相关接口。

2.2.1 bitset

首先是 bitset 相关的方法,first 返回 bitset 中第一个控制字节在当前 Group 中的相对索引。

go 复制代码
// first 返回集合中第一个控制字节在当前 Group 中的相对索引。
//
// 前置条件:bitset 不为空,也就是里面有数据
func (b bitset) first() uintptr {
	return bitsetFirst(b)
}

// first 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,直接计算末尾连续 0 的数量。
// 由于 bitset 已经是打包表示,因此不需要再进行移位,而通用架构上由于每个 slot
// 都是一个字节,所以计算出来之后需要 >> 3
// - 10000000   00000000   10000000   00000000
// -> 15 >> 3 = 1
// 也就是下标 1,第二个
func bitsetFirst(b bitset) uintptr {
	return uintptr(sys.TrailingZeros64(uint64(b))) >> 3
}

bitsetFirst 会计算出从右往左遇到第一个 1 之前,遇到了多少个 0,比如上面的例子算出来的就是 15,然后 15 >> 3 = 15 / 8 = 1,就是下标 1 的 slot。

go 复制代码
func (b bitset) removeFirst() bitset {
	return b & (b - 1)
}

上面的 removeFirst 就是清除掉第一个 1,也就是将最低位的 1 清除。

go 复制代码
// removeBelow 清除 Slot i 之前的所有已设置位,但不清除 Slot i。
func (b bitset) removeBelow(i uintptr) bitset {
	return bitsetRemoveBelow(b, i)
}

// removeBelow 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,用于清除低 i 位。
// Slot:    S3        S2        S1        S0
// bitset:  10000000  00000000  10000000  00000000
// 比如 i = 2,那么这个方法会清掉 S0 和 S1,保留 S2 和 S3
func bitsetRemoveBelow(b bitset, i uintptr) bitset {
	// 1. 首先计算出 i 的位移量: 8 * 2 = 16
	// 2. 1 << 16 - 1 -> 00000000 00000001 00000000 00000000
	// 3. mask = 00000000 00000000 11111111 11111111
	// 4. &^ 是 go 的特殊写法,代表 b & (!mask),也就是 and not
	mask := (uint64(1) << (8 * uint64(i))) - 1
	return b &^ bitset(mask)
}

removeBelow 用于清除 bitset 中所有下标 < i 的位,只保留下标 ≥ i 的位。

go 复制代码
// lowestSet 判断 bitset 中索引最低的 Slot 对应的位是否已设置,也就是 S0
//
// 该方法与 shiftOutLowest 配合使用,用于遍历所有 Slot,无论当前 Slot 是
// 否属于 bitset 表示的集合。
func (b bitset) lowestSet() bool {
	return bitsetLowestSet(b)
}

// lowestSet 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,用于检查 S0 是否设置了值
func bitsetLowestSet(b bitset) bool {
	return b&(1<<7) != 0
}

lowestSet 用于判断最低位是否已设置。

go 复制代码
// count 返回 b 中已设置 Slot 的数量,也就是统计一个 64 位整数的二进制表示中,
// 有多少个 bit 是 1。
func (b bitset) count() int {
	// AMD64 和通用 bitset 表示都适用。
	return sys.OnesCount64(uint64(b))
}

最后 count 用于计算 bitset 里面有多少个 1。

2.2.2 ctrlGroup

go 复制代码
// ctrlGroup 表示一个 Group 中的固定数量控制字节,
// 并将这些控制字节整体存储在一个 uint64 中。
type ctrlGroup uint64

// get 返回第 i 个控制字节。
func (g *ctrlGroup) get(i uintptr) ctrl {
	if goarch.BigEndian {
		return *(*ctrl)(unsafe.Add(unsafe.Pointer(g), 7-i))
	}
	return *(*ctrl)(unsafe.Add(unsafe.Pointer(g), i))
}

// set 设置第 i 个控制字节。
func (g *ctrlGroup) set(i uintptr, c ctrl) {
	if goarch.BigEndian {
		*(*ctrl)(unsafe.Add(unsafe.Pointer(g), 7-i)) = c
		return
	}
	*(*ctrl)(unsafe.Add(unsafe.Pointer(g), i)) = c
}

上面是最基础的 get,set 方法,我们之前说过,group 的布局结构是 【ctrlGroup】【slot0】【slot1】...【slot n】,所以计算第 i 个 slot 的状态的方法就是计算出 group 的地址再加上第 i 个 bit,也就是 unsafe.Add(unsafe.Pointer(g), i)。

go 复制代码
bitsetLSB   = 0x0101010101010101
bitsetEmpty = bitsetLSB * uint64(ctrlEmpty)

// setEmpty 将所有控制字节设置为空状态。
func (g *ctrlGroup) setEmpty() {
	*g = ctrlGroup(bitsetEmpty)
}

setEmpty 将所有控制字节设置为空状态,这里注意直接赋值给 *g,因为 group 的起始地址就是 ctrlsGroup 的地址。

go 复制代码
// matchH2 返回当前 Group 中满足以下条件的 Slot 集合:
// 1. Slot 处于已占用状态;
// 2. Slot 控制字节中的 7 位 H2 与给定值匹配。
// 该方法可能返回误匹配结果,因此调用方仍然必须比较完整的 key。
func (g ctrlGroup) matchH2(h uintptr) bitset {
	return ctrlGroupMatchH2(g, h)
}

// matchH2 的通用实现。参数 h 就是 h2,g 就是控制字段。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 返回值是 bitset 类型,其实就是 unit64
func ctrlGroupMatchH2(g ctrlGroup, h uintptr) bitset {
	// bitsetLSB:0x0101010101010101
	//         -> 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001
	// bitsetMSB: 0x8080808080808080
	//         -> 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000
	//
	// 1. bitsetLSB * uint64(h) 就是把 h 乘到 bitsetLSB 中,比如 h = 0x20,那么相乘之后结果就
	//    是 0x0202020202020202
	//    - 这里解释下,因为 bitsetLSB 是 0x0101010101010101,其实就是 0x01 * (2^0 + 2^8 + 2^16 + 2^24 + 2^32 + 2^40 + 2^48 + 2^56),
	//      所以 h = 0x20 相乘之后结果就是 0x0202020202020202,也就是 0x20 * (2^0 + 2^8 + 2^16 + 2^24 + 2^32 + 2^40 + 2^48 + 2^56),
	//      因此相乘之后就相当于将 h 复制到 8 个字节中,为什么不会产生进位?h 最大不过是 0xFF,且最关键的 bitsetLSB 相当于 1 * h,因此乘完
	//      之后最大就是 0xFFFFFFFF,不超过一个字节.
	// 2. uint64(g) ^ (bitsetLSB * uint64(h)) 计算异或值,我们之前说过,控制字段的值大概就是 0bxxxxxxxx xxxxxxxx xxxxxxxx ... 这种格式,
	//    每 8 位在 Slot 不为空的时候设置的就是 key 的 h2,进行 ^ 匹配,由于相同的会变成 0,因此如果能和这 8 个 Slot 其中某一个匹配上,那么
	//    肯定会出现 0bxxxxxxxx 00000000 ........ 这种情况.
	// 3. v - bitsetLSB 会把 00000000 变成 FFFFFFFF,也就是说符合条件的 slot 对应的 8 个 bit 就是 FFFFFFFF,比如 xxxxxxxx FFFFFFFF ........
	// 4. (v - bitsetLSB) &^ v 就是 (v - bitsetLSB) & (~v),由于 v 里面符合条件的 slot 是 00000000,所以 ~v 就是 FFFFFFFF,跟第三步的 &,
	//    结果就是让符合条件的保留 FFFFFFFF,其他的位不管是多少,- bitsetLSB 之后最高位肯定不是 1
	// 5. bitset(((v - bitsetLSB) &^ v) & bitsetMSB) 就是提取最高位了,这种情况下只有符合条件的 slot 最高位才是 1
	//
	v := uint64(g) ^ (bitsetLSB * uint64(h))
	return bitset(((v - bitsetLSB) &^ v) & bitsetMSB)
}

ctrlGroupMatchH2 用于返回一个 bitset,ctrlGroup 里面有 8 个 slot 的状态,这个方法返回的 bitset 就是标识 ctrlGroup 里面有哪些 slot 跟传进来的 h 匹配,这个方法用来匹配 h2 的,也就是根据 key 的 h2 快速判断这 8 个 slot 里面哪些 slot 是跟 h2 相等的,这些就有可能是我们要找的 key-value。

go 复制代码
// matchEmpty 返回当前 Group 中处于空状态的 Slot 集合。
func (g ctrlGroup) matchEmpty() bitset {
	return ctrlGroupMatchEmpty(g)
}

// matchEmpty 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchEmpty(g ctrlGroup) bitset {
	// 空 Slot:1 0 0 0 0 0 0 0
	// 已删除 Slot:1 1 1 1 1 1 1 0
	// 已占用 Slot:0 ? ? ? ? ? ? ?
	//
	// 一个 Slot 是不是空的,主要看第 1 位是不是 0 和第 7 位是不是 1
	// bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
	// << 6 之后 bit1 就去到了 bit7 的位置
	// &^ 就是 & (not (v << 6)),空 Slot 的 bit1 移动到 bit7 之后,经过 not 就是 1 了
	// (v &^ (v << 6)),求出结果再和 bitsetMSB 相 &,最高位如果是 1 就是 Empty 了
	v := uint64(g)
	return bitset((v &^ (v << 6)) & bitsetMSB)
}

matchEmpty 用于返回当前 Group 中处于空状态的 Slot 集合,空状态是 0b10000000。

java 复制代码
// matchEmptyOrDeleted 返回当前 Group 中处于空状态或已删除状态的 Slot 集合。
func (g ctrlGroup) matchEmptyOrDeleted() bitset {
	return ctrlGroupMatchEmptyOrDeleted(g)
}

// matchEmptyOrDeleted 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchEmptyOrDeleted(g ctrlGroup) bitset {
	// 空 Slot:1 0 0 0 0 0 0 0
	// 已删除 Slot:1 1 1 1 1 1 1 0
	// 已占用 Slot:0 ? ? ? ? ? ? ?
	//
	// 只要第 7 位为 1,Slot 就处于空状态或已删除状态。
	v := uint64(g)
	return bitset(v & bitsetMSB)
}

matchEmptyOrDeleted 用于返回当前 Group 中处于 Empty 或者 Deleted 状态的 Slot 集合,空状态是 0b10000000,deleted 状态是 0b11111110。

go 复制代码
bitsetMSB   = 0x8080808080808080

// anyFull 判断当前 Group 中是否至少有一个 Slot 处于已占用状态。
func (g ctrlGroup) anyFull() bool {
	// Slot 处于已占用状态的条件是第 7 位为 0。
	// 检查是否存在第 7 位为 0 的控制字节。
	return (^g)&bitsetMSB != 0
}

anyFull 判断当前 Group 中是否至少有一个 Slot 处于已占用状态,已占用的意思就是 0b0hhhhhhh,可以看到最高位是 0,所以 ^g & 10000000 != 0。

go 复制代码
// matchFull 返回当前 Group 中处于已占用状态的 Slot 集合。
func (g ctrlGroup) matchFull() bitset {
	return ctrlGroupMatchFull(g)
}

// matchFull 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchFull(g ctrlGroup) bitset {
	// 空 Slot:1 0 0 0 0 0 0 0
	// 已删除 Slot:1 1 1 1 1 1 1 0
	// 已占用 Slot:0 ? ? ? ? ? ? ?
	//
	// 只要第 7 位为 0,Slot 就处于已占用状态。
	v := uint64(g)
	return bitset(^v & bitsetMSB)
}

上面 anyFull 是返回当前 groups 里面是否有已经占用的 slot,这里的 matchFull 就是返回当前 groups 里面有哪些已经占用的 slot。

2.2.3 groupReference

下面是 group 的结构体描写,可以看到里面并没有完整把 slot 结构描述出来,而是只提供了 group 的起始地址,这是因为 slot 的具体类型要等到编译期才能确定------map 的 key/value 类型本来就是不确定的,所以 Go 是通过编译器和运行时提供的类型信息,来动态描述实际的 group 布局的。

一个 group 里包含两部分内容:控制字节和 slots。

  • 控制字节是一个 long 类型,由于一个 group 共有 8 个 slot,所以 long 里的每 8 bit 就代表一个 slot 的状态。控制字节并不存储完整的 key/value,它保存的是 slot 的摘要信息,主要用来解决两个问题:这个 slot 当前是什么状态,以及如果 slot 被占用,它对应的 key 有没有可能是我们要找的 key。
  • slots 部分则真正存放具体的 key-value,一个 group 有 8 个 slot。

下面具体说下控制字节。一个 slot 有三种状态:

  • 第一,这个 slot 是空的(Empty);
  • 第二,这个 slot 以前有元素,但已经被删除了(Deleted);
  • 第三,这个 slot 正在使用中(Full),此时会记录 key 哈希的相关信息。

控制字节和 slot 是一一对应的,C0 对应 S0,C1 对应 S1,依此类推。

当第一次往某个位置添加元素时,这个 slot 的状态就是 Full,此时它的控制字节形如 0xxxxxxx:最高位是 0,剩下的 7 bit 保存 key 哈希值的低 7 位,也就是 H2。比如某个 key 的 H2 是 0101010,那么这个 slot 的控制字节就是 00101010。

接下来如果删除元素,这个位置就会变成 0xFE,表示 Deleted 状态。为什么要这样处理呢?因为 Go 的 map 查询 key-value 用的是 线性探测法 :先根据 hash 找到一个 slot 下标,然后从这个位置开始线性往后探测,直到遇到空位才停止。如果删除元素时直接把这个位置改成 Empty,那么线性探测一旦走到这里就会停下,可实际上它后面可能还有数据,这样就会漏掉本该找到的元素。所以删除时用 0xFE 这个特殊的 Deleted 状态来占位,既标记了此处已删除,又不会让探测提前中断。

go 复制代码
type groupReference struct {
	// 下面就是指向分配出来的 group 内存,通过下面的 ctrls()/key()/elem() 访问地址对应的数据,
	// group 的整体结构如下
	//
	// type group struct {
	// 	ctrls ctrlGroup
	// 	slots [abi.MapGroupSlots]slot
	// }
	//
	// type slot struct {
	// 	key  typ.Key
	// 	elem typ.Elem
	// }
	//
	//  Group 起始地址
	// │
	// ├── ctrlGroup
	// │   ├── control byte 0
	// │   ├── control byte 1
	// │   ├── ...
	// │   └── control byte 7
	// │
	// ├── Slot 0
	// │   ├── key:string
	// │   └── elem:int
	// │
	// ├── Slot 1
	// │   ├── key:string
	// │   └── elem:int
	// │
	// ├── ...
	// │
	// └── Slot 7
	//     ├── key:string
	//     └── elem:int
	data unsafe.Pointer // data *typ.Group
}

我们接下来继续看下这个结构体对应的一些方法。

go 复制代码
/**
 * group 的内存布局如下:
 * - [ ctrls 控制字 ][ slot0 ][ slot1 ][ slot2 ]...
 *
 * 例如,假设某个 group 的起始地址是 0x1000,
 * 那么:
 * - ctrls() 返回的就是 0x1000 这 8 个字节对应的控制字区域
 * - key(typ, 0) 返回 slot0 中 key 的地址
 * - elem(typ, 0) 返回 slot0 中 elem 的地址
 * - key(typ, 1) 返回 slot1 中 key 的地址
 *
 * 通过下面的 ctrls / key / elem 方法,可以按 slot 下标访问这段连续内存。
 */
// ctrls 返回当前 group 的控制字节区域。
//
// 例子:
// - 如果 g.data 指向某个 group 的起始地址 0x1000,
//   那么 ctrls() 返回的就是位于 0x1000 开头的 ctrlGroup。
func (g *groupReference) ctrls() *ctrlGroup {
	return (*ctrlGroup)(g.data)
}

首先就是 ctrls,这个方法用来返回 group 的控制字节,因为整个 group 的起始地址就是 ctrls 的起始地址,所以直接获取地址然后转成 *ctrlGroup 类型的就行了。

go 复制代码
const (
	ctrlGroupsSize   = unsafe.Sizeof(ctrlGroup(0))
	groupSlotsOffset = ctrlGroupsSize
)

// key 返回下标 i 的 slot 元素的地址,而由于 key 在前面,所以这里返回的也是 key 的地址
func (g *groupReference) key(typ *abi.MapType, i uintptr) unsafe.Pointer {
	// [ctrls 控制字] + i * 每个 slot 占用的大小 就是第 i 个 slot 的地址偏移量
	offset := groupSlotsOffset + i*typ.SlotSize

	// 起始偏移量(groupReference 的地址) + 偏移量
	return unsafe.Pointer(uintptr(g.data) + offset)
}

// elem 内存布局是 [ key ][ elem ],ElemOff 是 slot 里面起始位置到 slot 的大小,
// 实际上这个方法返回的就是第 i 个 slot 里面 elem 的地址
func (g *groupReference) elem(typ *abi.MapType, i uintptr) unsafe.Pointer {
	offset := groupSlotsOffset + i*typ.SlotSize + typ.ElemOff

	return unsafe.Pointer(uintptr(g.data) + offset)
}

接下来就是 group 最重要的两个方法,查找 key 的地址和 value 的地址,key 的地址很简单,就是 ctrlGroupsSize + i * slotSize,因为 slot 的起始位置就是 key 的起始位置。elem 的计算差不多,要注意一点就是 ElemOff 是 elem 在 slot 里面的偏移量,所以直接加上就行了。

2.2.4 groupsReference

groupReference 是 group 结构体,这里就是 groups 结构体,存储多个 group,Map 直接引用的就是这个结构体。

go 复制代码
// groupsReference 是对一段连续 Group 数组的引用封装,map 里面引用 group 就是通过
// 下面的结构体,Group 数组的实际地址保存在 data 中
type groupsReference struct {
	// data 指向 Group 数组的起始地址,这里的实际类型可以理解为 *[length]typ.Group,
	// 其中每个 Group 的具体布局由 map 的 key/value 类型决定。
	data unsafe.Pointer // data *[length]typ.Group

	// lengthMask 等于 Group 数量减 1,因为 Group 数量必须是 2 的幂,因此可以使用 i
	// & lengthMask 代替较慢的取模运算 i % length
	lengthMask uint64
}

接下来就是初始化方法,也就是 new 方法。

go 复制代码
// 新建一个 group 数组,数组长度是 length
//
// Length must be a power of two.
func newGroups(typ *abi.MapType, length uint64) groupsReference {
	return groupsReference{
		// data = make([]typ.Group, length)
		// ┌──────────────────────┐
		// │ Group 0              │
		// │ ├── ctrlGroup        │
		// │ └── 8 个 Slot        │
		// ├──────────────────────┤
		// │ Group 1              │
		// │ ├── ctrlGroup        │
		// │ └── 8 个 Slot        │
		// ├──────────────────────┤
		// │ Group 2              │
		// │ ├── ctrlGroup        │
		// │ └── 8 个 Slot        │
		// ├──────────────────────┤
		// │ Group 3              │
		// │ ├── ctrlGroup        │
		// │ └── 8 个 Slot        │
		// └──────────────────────┘
		// 这里创建 groups,但是里面的 slot 没有初始化成 Empty,需要手动设置
		data: newarray(typ.Group, int(length)),

		// length - 1
		lengthMask: length - 1,
	}
}

最后一个方法,group 方法返回第 i 个 group 的起始位置。

go 复制代码
// group 返回第 i 个 group 的起始偏移量,groups 在内存中也是连续的
func (g *groupsReference) group(typ *abi.MapType, i uint64) groupReference {
	offset := uintptr(i) * typ.GroupSize

	return groupReference{
		data: unsafe.Pointer(uintptr(g.data) + offset),
	}
}

3. 小结

这篇文章简单学习了 group 里面的源码,下一篇文章就来看下 Map 整体的源码介绍。

如有错误,欢迎指出!!!

相关推荐
量化分析码农1 小时前
【Python量化波动率实战 01】波动率是怎么算的:年化、滚动与区间估计一次讲清
后端
特立独行的猫A1 小时前
AI 时代,我为什么推荐用 cj-tauri 开发桌面应用
java·后端·python
金銀銅鐵1 小时前
从一个类名到一张类图:用反射自动生成 PlantUML
java·后端·uml
追逐时光者2 小时前
一款基于 .NET 开源、免费的数据库自治诊断平台
后端·.net
追逐时光者2 小时前
让 GitHub Copilot 更懂你的 .NET 代码风格:一份精简实用的 Copilot Instructions
后端·.net
小满zs3 小时前
Go语言第十八章(架构设计)
后端·go
山甫aa3 小时前
锁一篇搞定:乐观锁、悲观锁、CAS、数据库锁与分布式锁【从零开始的 Web 后端学习】
java·数据库·分布式·后端·学习·web
IT_陈寒4 小时前
Java开发实战:从入门到精通
前端·人工智能·后端
jktantan4 小时前
注解式脱敏:用 Jackson 的 ContextualSerializer 做字段级策略(一)
spring boot·后端·算法·spring cloud·系统架构