深度解析GoLand map原理及实现,手撕源码!(一)——基本介绍,初始化,读

深度解析GoLand map原理及实现,手撕源码!(一)——基本介绍,初始化,读

  • 一、map
    • (1) map的初始化:
    • (2) map的基本方法:
    • (3) map的核心原理
    • (4) map源码详解
      • 4.1 结构体
        • 4.1.1 hmap
        • 4.1.2 mapextra
        • 4.1.3 bmap
      • 4.2 初始化 —— makemap
        • 4.2.1 hint 为 map 拟分配的容量;在分配前,会提前对拟分配的内存大小进行判断,倘若超限,会将 hint 置为零;
        • 4.2.2 通过 new 方法初始化 hmap;
        • 4.2.3 调用 fastrand,构造 hash 因子:hmap.hash0;
        • 4.2.4 调用overLoadFactor,基于 log2(B) >= hint 的思路,计算桶数组的容量 B;
        • 4.2.5 调用 makeBucketArray 方法,初始化桶数组 hmap.buckets;
        • 4.2.6 倘若 map 容量较大,会提前申请一批溢出桶 hmap.extra.
        • overLoadFactor
        • makeBucketArray
      • 4.3 读 —— mapaccess
        • 4.3.1 倘若 map 未初始化,或此时存在 key-value 对数量为 0,直接返回零值;
        • 4.3.2 倘若发现存在其他 goroutine 在写 map,直接抛出并发读写的 fatal error;其中,并发写标记,位于 hmap.flags 的第 3 个 bit 位;
        • 4.3.3 通过 maptype.hasher() 方法计算得到 key 的 hash 值,并对桶数组长度取模,取得对应的桶
        • 4.3.4 在取桶时,会关注当前 map 是否处于扩容的流程,倘若是的话,需要在老的桶数组 oldBuckets 中取桶,通过 evacuated 方法判断桶数据是已迁到新桶还是仍存留在老桶,倘若仍在老桶,需要取老桶进行遍历.
        • 4.3.5 取 key hash 值的高 8 位值 top. 倘若该值 < 5,会累加 5,以避开 0 ~ 4 的取值. 因为这几个值会用于枚举,具有一些特殊的含义.
        • 4.3.6 开启两层 for 循环进行遍历流程,外层基于桶链表,依次遍历首个桶和后续的每个溢出桶,内层依次遍历一个桶内的 key-value 对.

一、map

Map 是一种无序的键值对的集合。

Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

读、写、删除操作控制,时间复杂度为O(1)

(1) map的初始化:

map中,key的数据类型必须是可比较的类型 slice,map,func不可比较

//使用make
m := makemap[string]int//使用字面量
m := map[string]int{}

(2) map的基本方法:

  • 获取元素: 如果键不存在,ok 的值为 false,value值为该类型的零值
value1 := m["first"]
value2,ok := m["second"]
  • 修改元素
m["first"] = 100
  • 删除元素
delete(m,"first")
  • 遍历map:是无序的
for k, v := range m {fmt.Printf("key=%s, value=%d\n", k, v)
}
  • 获取map长度
len := len(m)
  • 并发

map 不是并发安全的数据结构 具体规则是:
(1)并发读没有问题;
(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;
(3)读的时候发现其他 goroutine 在并发写,抛出 fatal error;
(4)写的时候发现其他 goroutine 在并发写,抛出 fatal error.

fatal("concurrent map read and map write")
fatal("concurrent map writes")

此处并发读写会引发 fatal error,是一种比 panic 更严重的错误,无法使用 recover 操作捕获.

(3) map的核心原理

基本上hash map的核心原理可以总结为以下三步

  1. 通过哈希方法获得key的哈希值
  2. 哈希值对桶数组的长度取模,确定其所属的桶
  3. 在桶中插入 key-value对

当然了,桶数组是有限的,而数据量却是无限的,必然会发生哈希冲突,现在通常使用的有两种方法:

  • 拉链法——简单常用;无需预先为元素分配内存
  • 开放寻址法——无需额外的指针用于链接元素;内存地址完全连续,可以基于局部性原理,充分利用 CPU 高速缓存。

实际上,在map解决冲突问题的时候,结合了这两种思路,在后文详解源码的时候说,这里线暂时按下不表。

光解决hash冲突问题还是不够的,为了保证常数的时间复杂度,map还引入了扩容机制,常用的有两种,增量扩容和等量扩容。

  • 当桶内 key-value 总数/桶数组长度 > 6.5 时发生增量扩容,桶数组长度增长为原值的两倍;
  • 当桶内溢出桶数量大于等于 2^B 时( B 为桶数组长度的指数,B 最大取 15),发生等量扩容,桶的长度保持为原值;
  • 为了避免性能抖动,采取了渐进扩容的方式

(4) map源码详解

源码位置在 \src\runtime\map.go
主要分为六大流程

  • 初始化
  • 遍历
  • 扩容

4.1 结构体

在进行源码分析之前,我们先要了解其中用到的一些结构体

4.1.1 hmap

源代码带有部分注释,下面是翻译+补充后的注释

type hmap struct {// 注意:hmap 的格式也在 cmd/compile/internal/reflectdata/reflect.go 中编码。// 请确保此处与编译器定义保持同步。count     int // map 中的 key-value 总数  必须放在第一位(被 len() 内置函数使用)flags     uint8 // map 状态标识,可以标识出 map 是否被 goroutine 并发读写;B         uint8  // 桶数组长度的指数 (可以容纳 loadFactor * 2^B 个元素)noverflow uint16 // 溢出桶的近似数量;有关详细信息,请参见 incrnoverflowhash0     uint32 // hash 随机因子,生成 key 的 hash 值时会使用到;buckets    unsafe.Pointer // 2^B 个 桶数组。如果 count==0,则可能为 nil。oldbuckets unsafe.Pointer // 老桶数组,大小为当前大小的一半,仅在扩容时非 nilnevacuate  uintptr        // 扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中;extra *mapextra // 预申请的溢出桶.
}
4.1.2 mapextra
type mapextra struct {// 如果键和元素都不包含指针并且是内联的,那么我们将标记 bucket 类型为不包含指针。// 这避免了扫描这样的映射。// 然而,bmap.overflow 是一个指针。为了保持溢出桶的存活,// 我们在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中存储指向所有溢出桶的指针。// overflow 和 oldoverflow 仅在键和元素不包含指针时使用。// overflow 包含 hmap.buckets 的溢出桶。// oldoverflow 包含 hmap.oldbuckets 的溢出桶。// 间接性允许在 hiter 中存储指向切片的指针。overflow    *[]*bmapoldoverflow *[]*bmap// nextOverflow 保存指向一个空闲溢出桶的指针nextOverflow *bmap
}

上面这个注释可能看不太明白,简单解释一下:

  • mapextra.overflow:供桶数组 buckets 使用的溢出桶;

  • mapextra.oldoverFlow: 扩容流程中,供老桶数组 oldBuckets 使用的溢出桶;

  • mapextra.nextOverflow:下一个可用的溢出桶;

4.1.3 bmap
type bmap struct {// tophash 通常包含此桶中每个键的哈希值的顶部字节。// 如果 tophash[0] < minTopHash,// 那么 tophash[0] 就代表一个桶扩容状态。tophash [bucketCnt]uint8// 之后是 bucketCnt 个键,然后是 bucketCnt 个元素。// 注意:将所有键打包在一起,然后是所有元素打包在一起,使得// 代码比交替 key/elem/key/elem/... 更复杂一些,但它允许// 我们消除可能需要的填充,例如,对于 map[int64]int8。// 之后是一个溢出指针。
}

这个可能更抽象了,简单解释一下:

  • bmap就是map,存储了8个键值对以及一个溢出桶指针
  • 键值对不仅仅包含 key 和 value ,还包含了一个顶哈希值(就是代码中的tophash),这个值主要方便后续通过内存地址偏移的方式来寻找key数组、value数组以及溢出桶指针
  • 源码中只展示了tophash部分,为方便理解,给大家一个补全后的源码:
const bucketCnt = 8  // 每个桶可容纳的最大的键值对的数量
//源码中是这样定义这个常量的:
//bucketCnt = abi.MapBucketCount
//MapBucketCount = 1 << MapBucketCountBits
//MapBucketCountBits = 3
//合成一下就是 bucketCnt = 8 
type bmap struct {tophash [bucketCnt]uint8keys [bucketCnt]Tvalues [bucketCnt]Toverflow uint8
}

4.2 初始化 —— makemap

map的初始化是通过调用makemap方法实现的,下面是源码:

// makemap 用于实现 Go 语言中的 map 创建,对应 make(map[k]v, hint)。
// 如果编译器确定 map 或第一个 bucket 可以在栈上创建,
// 那么 h 和/或 bucket 可能是非 nil。
// 如果 h != nil,map 可以直接在 h 中创建。
// 如果 h.buckets != nil,指向的 bucket 可以用作第一个 bucket。
func makemap(t *maptype, hint int, h *hmap) *hmap {mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)if overflow || mem > maxAlloc {hint = 0}// 初始化 Hmapif h == nil {h = new(hmap)}h.hash0 = fastrand()// 找到能够容纳请求元素数量的大小参数 B。// 对于 hint < 0,overLoadFactor 返回 false,因为 hint < bucketCnt。B := uint8(0)for overLoadFactor(hint, B) {B++}h.B = B// 分配初始哈希表// 如果 B == 0,buckets 字段会在稍后懒加载(在 mapassign 中)// 如果 hint 很大,清零这块内存可能需要一段时间。if h.B != 0 {var nextOverflow *bmaph.buckets, nextOverflow = makeBucketArray(t, h.B, nil)if nextOverflow != nil {h.extra = new(mapextra)h.extra.nextOverflow = nextOverflow}}return h
}

主要就是6步走:

4.2.1 hint 为 map 拟分配的容量;在分配前,会提前对拟分配的内存大小进行判断,倘若超限,会将 hint 置为零;
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {hint = 0
}
  • 先将hint转为无符号整型(uintptr),乘上每个桶的大小(t.bucket.size),得到map分配的总内存大小(mem);并判断结果是否超出无符号整型(uintptr)所能表示的最大值,将这个布尔值赋给overflow
  • 如果超出了无符号整型的最大值,或者超出了最大可分配内存大小(maxAlloc),就将hint赋值为0,方便后续操作
4.2.2 通过 new 方法初始化 hmap;
if h == nil {h = new(hmap)
}
4.2.3 调用 fastrand,构造 hash 因子:hmap.hash0;
h.hash0 = fastrand()
4.2.4 调用overLoadFactor,基于 log2(B) >= hint 的思路,计算桶数组的容量 B;
B := uint8(0)
for overLoadFactor(hint, B) {B++
}
h.B = B
4.2.5 调用 makeBucketArray 方法,初始化桶数组 hmap.buckets;
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
4.2.6 倘若 map 容量较大,会提前申请一批溢出桶 hmap.extra.
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)

源码中涉及到了两个新方法:overLoadFactor、makeBucketArray

overLoadFactor

通过 overLoadFactor 方法,对 map 预分配容量和桶数组长度指数进行判断,决定是否仍需要增长 B 的数值:

const loadFactorNum = 13
//这个数实际应为12 为方便理解,可以看作13
//计算过程如下:
// loadFactorNum = (bucketCnt * 13 / 16) * loadFactorDen
// bucketCnt =  8  前面计算过这个值了
// loadFactorDen = 2
//所以 loadFactorNum = (8 * 13 / 16) * 2 = 12
const loadFactorDen = 2
const goarch.PtrSize = 8
const bucketCnt = 8
// overLoadFactor 判断将 count 个元素放入 1<<B 个桶中是否超过了负载因子
func overLoadFactor(count int, B uint8) bool {return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}// bucketShift返回1<<b,为代码生成优化。
func bucketShift(b uint8) uintptr {// 通过掩码操作来限制移位的数量,可以省略溢出检查。return uintptr(1) << (b & (goarch.PtrSize*8 - 1))
}
  • 倘若 map 预分配容量小于等于 8,B 取 0,桶的个数为 1;

  • 保证 map 预分配容量小于等于桶数组长度 * 6.5

map 预分配容量、桶数组长度指数、桶数组长度之间的关系如下表:

kv 对数量桶数组长度指数 B桶数组长度 2^B
0 ~ 801
9 ~ 1312
14 ~ 2624
27 ~ 5238
2^(B-1) * 6.5+1 ~ 2^B*6.5B2^B
makeBucketArray

makeBucketArray 方法会进行桶数组的初始化,并根据桶的数量决定是否需要提前作溢出桶的初始化:

// makeBucketArray为map桶初始化一个后备数组。
// 1<<b是要分配的最小桶数。
// dirtyalloc应该是nil,或者是之前由具有相同t和b参数的makeBucketArray分配的桶数组。
// 如果dirtyalloc为nil,则将分配一个新的后备数组,否则将清除dirtyalloc并将其重用作后备数组。
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {base := bucketShift(b)nbuckets := base// 对于较小的b,溢出桶的可能性较小。// 避免计算的开销。if b >= 4 {// 添加上估计的溢出桶的数量// 以插入具有此b值的中位数元素所需的数量。nbuckets += bucketShift(b - 4)sz := t.Bucket.Size_ * nbucketsup := roundupsize(sz)if up != sz {nbuckets = up / t.Bucket.Size_}}if dirtyalloc == nil {buckets = newarray(t.Bucket, int(nbuckets))} else {// dirtyalloc先前由上述newarray(t.Bucket, int(nbuckets))生成,但可能不为空。buckets = dirtyallocsize := t.Bucket.Size_ * nbucketsif t.Bucket.PtrBytes != 0 {memclrHasPointers(buckets, size)} else {memclrNoHeapPointers(buckets, size)}}if base != nbuckets {// 我们预先分配了一些溢出桶。// 为了将跟踪这些溢出桶的开销降到最低,// 我们使用的约定是,如果预先分配的溢出桶的溢出// 指针为nil,则通过增加指针来获得更多的溢出桶。// 我们需要一个安全的非nil指针,用于最后一个溢出桶;只需使用buckets。nextOverflow = (*bmap)(add(buckets, base*uintptr(t.BucketSize)))last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.BucketSize)))last.setoverflow(t, (*bmap)(buckets))}return buckets, nextOverflow
}
  • makeBucketArray 会为 map 的桶数组申请内存,在桶数组的指数 b >= 4时(桶数组的容量 >= 52 ),会需要提前创建溢出桶,通过 base 记录桶数组的长度,不包含溢出桶;通过 nbuckets 记录累加上溢出桶后,桶数组的总长度.
base := bucketShift(b)
nbuckets := base
if b >= 4 {nbuckets += bucketShift(b - 4)
}
  • 如果没有提供现有的桶数组,那么就调用newarray函数来创建一个新的桶数组。
buckets = newarray(t.bucket, int(nbuckets))
  • 如果已经有一个先前分配的桶数组。这个数组可能已经包含了一些数据,所以需要被清空。(清空方式根据桶中是否有指针类型字段来判断:有指针就选用memclrHasPointers,没指针就选用memclrNoHeapPointers)
buckets = dirtyalloc
size := t.Bucket.Size_ * nbuckets
if t.Bucket.PtrBytes != 0 {memclrHasPointers(buckets, size)
} else {memclrNoHeapPointers(buckets, size)
}
  • 如果需要创建溢出桶,会基于地址偏移的方式,通过 nextOverflow 指向首个溢出桶的地址。
if base != nbuckets {nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow //返回桶数组的起始地址和下一个溢出桶的地址。

backets:桶数组的起始地址
base:当前桶的索引
uintptr(t.bucketsize):每个桶的大小
nbuckets:桶的总数

  • 如果不需要创建溢出桶,会在将最后一个溢出桶的 overflow 指针指向 buckets 数组,以此来标识申请的溢出桶已经用完。
func (b *bmap) setoverflow(t *maptype, ovf *bmap) {*(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-goarch.PtrSize)) = ovf
}

unsafe.Pointer(b): 当前桶b的指针
goarch.PtrSize:当前操作系统架构下指针的大小

4.3 读 —— mapaccess

读流程主要分为以下几步:

  1. 根据 key 取 hash 值;
  2. 根据 hash 值对桶数组取模,确定所在的桶;
  3. 沿着桶链表依次遍历各个桶内的 key-value 对;
  4. 命中相同的 key,则返回 value;倘若 key 不存在,则返回零值;

源码如下:

// mapaccess1返回一个指向h[key]的指针。它永远不会返回nil,如果
// key不在map中,它将返回一个引用elem类型的零对象。
// 注意:返回的指针可能会使整个map保持活动状态,所以不要
// 长时间持有它。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {// 如果启用了竞态检测并且h不为nil,进行竞态检测。if raceenabled && h != nil {callerpc := getcallerpc()pc := abi.FuncPCABIInternal(mapaccess1)racereadpc(unsafe.Pointer(h), callerpc, pc)raceReadObjectPC(t.Key, key, callerpc, pc)}// 如果启用了内存清理检测并且h不为nil,进行内存读取检测。if msanenabled && h != nil {msanread(key, t.Key.Size_)}// 如果启用了地址清理检测并且h不为nil,进行地址读取检测。if asanenabled && h != nil {asanread(key, t.Key.Size_)}// 如果h为nil或者h的计数为0,返回零值的引用。if h == nil || h.count == 0 {if t.HashMightPanic() {t.Hasher(key, 0) // 见 issue 23734}return unsafe.Pointer(&zeroVal[0])}// 如果map正在写入,抛出致命错误。if h.flags&hashWriting != 0 {fatal("concurrent map read and map write")}// 计算key的哈希值。hash := t.Hasher(key, uintptr(h.hash0))// 计算桶掩码。m := bucketMask(h.B)// 定位到key所在的桶。b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.BucketSize)))// 如果有旧桶存在,检查是否需要从旧桶中查找。if c := h.oldbuckets; c != nil {if !h.sameSizeGrow() {// 旧桶数量是当前桶数量的一半;向下调整一个二的幂次。m >>= 1}oldb := (*bmap)(add(c, (hash&m)*uintptr(t.BucketSize)))if !evacuated(oldb) {b = oldb}}// 计算顶部哈希值。top := tophash(hash)// 遍历桶和溢出桶,查找key。
bucketloop:for ; b != nil; b = b.overflow(t) {for i := uintptr(0); i < bucketCnt; i++ {// 如果顶部哈希值不匹配,继续查找。if b.tophash[i] != top {if b.tophash[i] == emptyRest {break bucketloop}continue}// 定位到key的存储位置。k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))if t.IndirectKey() {k = *((*unsafe.Pointer)(k))}// 如果key匹配,定位到value的存储位置并返回。if t.Key.Equal(key, k) {e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))if t.IndirectElem() {e = *((*unsafe.Pointer)(e))}return e}}}// 如果没有找到key,返回零值的引用。return unsafe.Pointer(&zeroVal[0])
}

核心代码详解:

4.3.1 倘若 map 未初始化,或此时存在 key-value 对数量为 0,直接返回零值;
if h == nil || h.count == 0 {return unsafe.Pointer(&zeroVal[0])
}
4.3.2 倘若发现存在其他 goroutine 在写 map,直接抛出并发读写的 fatal error;其中,并发写标记,位于 hmap.flags 的第 3 个 bit 位;
 const hashWriting  = 4if h.flags&hashWriting != 0 {fatal("concurrent map read and map write")}
4.3.3 通过 maptype.hasher() 方法计算得到 key 的 hash 值,并对桶数组长度取模,取得对应的桶
 hash := t.hasher(key, uintptr(h.hash0))m := bucketMask(h.B)b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))

其中,bucketMast 方法会根据 B 求得桶数组长度 - 1 的值,用于后续的 & 运算,实现取模的效果:

eg: %8和&7效果是一致的,都是返回除以8的余数 解释如下:

&是位与运算符,它对两个数的二进制表示进行位与操作。当使用a & 7时,由于7的二进制表示是0111,这个操作实际上保留了a的二进制表示的最后三位,并将其他位设置为0。这相当于计算a除以8的余数,因为最后三位二进制数表示的值的范围也是从0到7

func bucketMask(b uint8) uintptr {return bucketShift(b) - 1
}
4.3.4 在取桶时,会关注当前 map 是否处于扩容的流程,倘若是的话,需要在老的桶数组 oldBuckets 中取桶,通过 evacuated 方法判断桶数据是已迁到新桶还是仍存留在老桶,倘若仍在老桶,需要取老桶进行遍历.
 if c := h.oldbuckets; c != nil {if !h.sameSizeGrow() {m >>= 1}oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))if !evacuated(oldb) {b = oldb}}

在取老桶前,会先判断 map 的扩容流程是否是增量扩容,倘若是的话,说明老桶数组的长度是新桶数组的一半,需要将桶长度值 m 除以 2.

const (sameSizeGrow = 8
)func (h *hmap) sameSizeGrow() bool {return h.flags&sameSizeGrow != 0
}

取老桶时,会调用 evacuated 方法判断数据是否已经迁移到新桶. 判断的方式是,取桶中首个 tophash 值,倘若该值为 2,3,4 中的一个,都代表数据已经完成迁移.

const emptyOne = 1 //空元素的顶部哈希值
const evacuatedX = 2
const evacuatedY = 3
const evacuatedEmpty = 4 
const minTopHash = 5 //最小的有效顶部哈希值func evacuated(b *bmap) bool {h := b.tophash[0]return h > emptyOne && h < minTopHash
}
4.3.5 取 key hash 值的高 8 位值 top. 倘若该值 < 5,会累加 5,以避开 0 ~ 4 的取值. 因为这几个值会用于枚举,具有一些特殊的含义.
const minTopHash = 5top := tophash(hash)func tophash(hash uintptr) uint8 {top := uint8(hash >> (goarch.PtrSize*8 - 8))if top < minTopHash {top += minTopHash}return top
4.3.6 开启两层 for 循环进行遍历流程,外层基于桶链表,依次遍历首个桶和后续的每个溢出桶,内层依次遍历一个桶内的 key-value 对.
bucketloop:
for ; b != nil; b = b.overflow(t) {for i := uintptr(0); i < bucketCnt; i++ {// ...}
}
return unsafe.Pointer(&zeroVal[0])

内存遍历时,首先查询高 8 位的 tophash 值,看是否和 key 的 top 值匹配.

倘若不匹配且当前位置 tophash 值为 0,说明桶的后续位置都未放入过元素,当前 key 在 map 中不存在,可以直接打破循环,返回零值.

const emptyRest = 0
if b.tophash[i] != top {if b.tophash[i] == emptyRest {break bucketloop}continue
}

倘若找到了相等的 key,则通过地址偏移的方式取到 value 并返回.

其中 dataOffset 为一个桶中 tophash 数组所占用的空间大小.

if t.key.equal(key, k) {e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))return e
}

倘若遍历完成,仍未找到匹配的目标,返回零值兜底.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/834000.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《intel开发手册卷1》学习笔记1

1、操作模式 IA-32架构支持三种基本操作模式:保护模式、实地址模式和系统管理模式。操作模式决定了哪些指令和体系结构功能是可访问的: 1&#xff09;保护模式&#xff1a;该模式是处理器的自然状态。保护模式的功能之一是能够在受保护的多任务环境中直接执行“实地址模式”80…

Mac 电脑安装 Raptor 流程图软件的方法

0. 安装逻辑 &#xff08;1&#xff09;运行 raptor&#xff0c;本质上需要 mac 能够运行 windows 程序&#xff0c;因此需要安装 .NET Runtime 7.0&#xff0c;这是微软程序运行必须的文件。 &#xff08;2&#xff09;运行 raptor 还需要安装依赖文件 mono-libgdiplus。 &am…

RabbitMQ - 以 MQ 为例,手写一个 RPC 框架 demo

目录 前言 一、再谈自定义应用层协议 二、再谈 BrokerServer 三、再谈 Connection、Channel 四、Demo a&#xff09;启动服务器 b&#xff09;客户端连接 前言 本篇文章来自于笔者之前写过的一个系列 —— “根据源码&#xff0c;模拟实现 RabbitMQ” 系列&#xff0c…

CentOs9编译C指令报错的一种解决方案

今天使用centos9编译c代码时&#xff0c;显示bash: gcc: command not found... 下图是我的报错页面&#xff0c;依据提示信息安装gcc之后依旧显示失败 找到其中一种解决方式&#xff0c;完美解决&#xff0c;供参考 输入以下指令更新软件包列表&#xff0c;这里需要等待几分…

MT3031 AK IOI

思路&#xff1a;把每个节点存到堆&#xff08;大根堆&#xff09;里。 如果节点放入后总时间没有超过m则放入堆中&#xff1b;如果总时间超过了&#xff0c;就看堆头元素是否比新元素大。如果大&#xff0c;则删除堆头&#xff08;反悔贪心&#xff09;。 注意别忘记开long l…

keystone学习小结

1 keystone middleware 1.1 工作流程 middleware在客户端和服务端之间&#xff0c;会拦截客户端请求并判断请求身份是否是正确合法的&#xff0c;若是&#xff0c;则继续将请求发给其他middleware或app 具体看&#xff0c;干了这些事 1将请求里的auth header去除&#xff0c…

爬虫:爬取豆瓣电影

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 上篇我们将到如何利用xpath的规则&#xff0c;那么这一次&#xff0c;我们将通过案例来告诉读者如何使用Xpath来定位到我们需要的数据&#xff0c;就算你不懂H5代码是怎么个嵌套或者十分复…

C++新特性-线程

主要内容 thread、condition、mutexatomicfunction、bind使用新特性实现线程池&#xff08;支持可变参数列表&#xff09;异常协程其他 1 C11多线程thread 重点&#xff1a; join和detach的使用场景thread构造函数参数绑定c函数绑定类函数线程封装基础类互斥锁mutexconditi…

计算机组成原理网课笔记

无符号整数的表示与运算 带符号整数的表示与运算 原反补码的特性对比 移码

搜狗输入法 PC端 v14.4.0.9307 去广告绿化版.

软件介绍 搜狗拼音输入法作为众多用户计算机配置的必备工具&#xff0c;其功能的全面性已为众所周知&#xff0c;并且以其高效便捷的输入体验受到广大使用者的青睐。然而&#xff0c;该软件在提供便利的同时&#xff0c;其内置的广告元素常常为用户带来一定的干扰。为此&#…

Linux中动态库的用法及优缺点?怎样制作动态库和静态库?

一、什么是gcc gcc的全称是GNU Compiler Collection&#xff0c;它是一个能够编译多种语言的编译器。最开始gcc是作为C语言的编译器&#xff08;GNU C Compiler&#xff09;&#xff0c;现在除了c语言&#xff0c;还支持C、java、Pascal等语言。gcc支持多种硬件平台. 在 Linux…

航空电子ARINC818采集卡

ARINC818采集卡是针对航空电子数字视频总线协议&#xff08;Avionics Digital Video BUS&#xff0c;ADVB&#xff09;的高性能PCIe视频光纤采集测试设备。ARINC818协议主要应用于机载设备间的实时高清图像传输&#xff0c;目前已经成功应用于多款民用、军用机型当中&#xff0…

渐进淡出背景个人导航页源码(火影版)

渐进淡出背景个人导航页源码&#xff08;火影版&#xff09; 效果图部分源码领取源码下期更新预报 效果图 部分源码 <!DOCTYPE html> <html> <head> <!--小K网 www.xkwo.com --><meta charset"UTF-8"><title>火影版个人主页<…

每日一博 - 闲聊架构设计中的多级缓存设计

文章目录 方法论概述客户端缓存应用层缓存服务层缓存缓存设计的注意事项总结 思维导图戳这里 方法论概述 从客户端到服务层&#xff0c;缓存的应用广泛而重要。通过合理的缓存设计&#xff0c;能够有效地提高系统的性能并降低延迟。 客户端缓存 在客户端层面&#xff0c;浏览…

多模态融合技术现实世界中的挑战与研究进展

在人工智能的诸多领域中&#xff0c;多模态融合技术正逐渐成为连接不同信息源的桥梁。这种技术通过整合来自视觉、听觉、文本等多种模态的数据&#xff0c;旨在提供更为丰富和精确的预测结果。然而&#xff0c;现实世界的数据往往是不完美和不完整的&#xff0c;这给多模态融合…

[微信小程序] 入门笔记1-滚动视图组件

[微信小程序] 入门笔记1-滚动视图组件 1.页面&组件&渲染 在小程序是由一个个页面page组成, 而页面又是由一个个组件component组成.和网页类似,这里的组件指的就是输入框<input>,按钮<button>,文本<text>,图片<image>等元素.如果你学过网页一…

Linux基础之git与调试工具gdb

目录 一、git的简单介绍和使用方法 1.1 git的介绍 1.2 git的使用方法 1.2.1 三板斧之git add 1.2.2 三板斧之git commit 1.2.3 三板斧之git push 二、gdb的介绍和一些基本使用方法 2.1 背景介绍 2.2 基本的使用方法 一、git的简单介绍和使用方法 1.1 git的介绍 Git是一…

Shell 编程规范与变量

目录 一.Shell 1.shell 的概念 2.Linux 中有哪些 Shell &#xff1f; 二.Shell 脚本概述 1.Shell 脚本的概念 2.shell 脚本应用场景 3.shell 脚本的作用 三.Shell脚本的构成与执行 1.Shell脚本的构成 2.Shell脚本的执行 四.重定向与管道操作 1.交互式硬件设备 2.重…

论文分享[cvpr2018]Non-local Neural Networks非局部神经网络

论文 https://arxiv.org/abs/1711.07971 代码https://github.com/facebookresearch/video-nonlocal-net 非局部神经网络 motivation:受计算机视觉中经典的非局部均值方法[4]的启发&#xff0c;非局部操作将位置的响应计算为所有位置的特征的加权和。 非局部均值方法 NLM&#…

男士内裤什么牌子的好?男士内裤五大排名品牌推荐

夏天快到了&#xff0c;你是不是在为内裤不够舒适透气、质量不好而困扰呢&#xff1f;现在市面上的男士内裤品牌众多&#xff0c;而且还有各种材质的分类&#xff0c;让大家一时也不知道该选什么好。 那么最近我也是特别购置了近期热门的几个男士内裤品牌进行测评&#xff0c;今…