golang -- map实现原理

目录

  • 一、前言
  • 二、结构
    • 1. hmap(map) 结构
    • 2. bmap(buckets) 结构
  • 三、哈希冲突
  • 四、负载因子
  • 五、哈希函数
  • 六、扩容
    • 增量扩容
    • 等量扩容

一、前言

在现代编程语言中,map 是一种非常重要的数据结构,广泛用于存储和快速查找键值对。Go 语言中的 map 是一种高效且灵活的数据结构,它不仅提供了基本的键值存储功能,还在并发场景下具备了出色的性能表现

现在探讨 Go 语言中 map 的实现原理,分析它的扩容策略、哈希计算方式等细节

二、结构

我对 map 的理解就是
一个指针,指向一个数组
这个数组呢,存储的是结构体
这个结构体里面呢,有几个元素
也就是,一个指向哈希桶数组的指针
一个哈希桶可以存储多个键值对

这张图现在可能不理解,因为还没有了解 map 的结构,没关系,看了后面再有这张图会好理解很多
在这里插入图片描述

1. hmap(map) 结构

go 中的 map 就是 hmap 结构体,源码在 runtime/map.go:hmap

type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)clearSeq   uint64extra *mapextra // optional fields
}

一些字段的解释(有几个字段 只看干巴巴的文字解释没有具体用法不太好理解,到这里理解count、B、buckets 就可以,不理解的没必要死磕,可以先跳过去,用到的时候再来看,不影响阅读):

  • count:表示 hamp 中的元素数量,就是 len(map)

  • flags,hmap :的标志位,用于表示 hmap 处于什么状态,有以下几种可能。

const (iterator     = 1 // 迭代器正在使用桶oldIterator  = 2 // 迭代器正在使用旧桶hashWriting  = 4 // 一个协程正在写入hmapsameSizeGrow = 8 // 正在等量扩容
)
  • B :map中的哈希桶的数量为1 << B (2^B 2的B次方)
    这里 2^B 和 count 有一定关系,但不相等,可以理解为:count代表的是所有桶中存储的所有键值对的个数,而2^B仅仅代表哈希桶的数量,而且一个哈希桶里面不只有一个键值对

  • noverflow : map 中溢出桶的大致数量

  • hash0 : 哈希种子,在 hmap 被创建时指定,用于计算哈希值(key)

  • buckets :指向哈希桶数组的指针

  • oldbuckets : 存放 hmap 在扩容前哈希桶数组的指针

  • extra : 存放着 hmap 中的溢出桶,溢出桶指的是就是当前桶已经满了,创建新的桶来存放元素,新创建的桶就是溢出桶

2. bmap(buckets) 结构

hmap 中的 buckets 也就是桶切片指针,go 中的 buckets 就是 bmap 结构体,定义为:

type bmap struct {tophash [abi.OldMapBucketCount]uint8
}

bmap 中只有一个 tophash 字段(其实还有很多注释,这里都删掉了)
tophash 存储的是每个键的哈希值的高 8 位。
当查找某个键时,通过只比较高 8 位来过滤掉一些键,避免了每次查找时都进行完整的哈希值比较,由此:它可以快速比较键值对,以便在哈希表中进行查找。

实际上,由于 map 可以存储各种类型的键值对,不同的类型占用不同的内存空间,所以需要在编译时根据类型来推导占用的内存空间,并且 map 的 key 必须是可比较的,所以也会判断 key 是否满足要求,由此可以推断 bmap 中不止有这一个字段

所以实际上,bmap的结构如下,不过这些字段对我们是不可见的,go 在实际操作中是通过移动 unsafe 指针来进行访问

type bmap struct {tophash [BUCKETSIZE]uint8keys [BUCKETSIZE]keyTypeelems [BUCKETSIZE]elemTypeoverflow *bucket
}

其中的一些解释如下
tophash,存放每一个键的高八位值,对于一个 tophash 的元素而言,有下面几种特殊的值

const (emptyRest      = 0 // 当前元素是空的,并且该元素后面也没有可用的键值了emptyOne       = 1 // 当前元素是空的,但是该元素后面有可用的键值。evacuatedX     = 2 // 扩容时出现,只能出现在oldbuckets中,表示当前元素被搬迁到了新哈希桶数组的上半区evacuatedY     = 3 // 扩容时出现只能出现在oldbuckets中,表示当前元素被搬迁到了新哈希桶数组的下半区evacuatedEmpty = 4 // 扩容时出现,元素本身就是空的,在搬迁时被标记minTopHash     = 5 // 对于一个正常的键值来说tophash的最小值
)

只要是tophash[i]的值大于minTophash的值,就说明对应下标存在正常的键值。

keys:存储 key

elems:存储 value

overflow:指向溢出桶的指针


三、哈希冲突

哈希冲突 并不陌生,在数据结构哈希表里面就有了哈希冲突这个定义
说白了就是有不止一个键 (k1, k2, …),计算出它们的哈希值相同,这些键就是发生了冲突
我们使用拉链法处理冲突,和数据结构中哈希桶处理方法一样。就是你有一个桶,这个桶里原本已经有元素了,但是又有其他的键映射到这个桶里面,那就把这个新的元素和旧元素链起来,我认为理解成把元素一个一个挂在哈希桶下面比较好理解
当然了,这种处理方法是有缺陷的
数据量少的时候,假设有 4 个哈希桶,有 12 个元素,那么平均就是一个桶里存储 3 个元素,查找一个键,计算出这个键对应的哈希值,就知道了这个元素在哪个桶,再遍历这个桶,最坏就是要查找的键挂在桶的最下面,那遍历三次就可以找到了。所以一个桶里面存储的数据少的时候,时间复杂度就是常量级别的
但是当数据量很大很大的时候,有 100亿, 这时候如果有 4 个桶, 一个桶里面平均存储 25亿 个元素,再去查找元素,再去遍历桶,那就很慢很慢了
所以哈希桶下面可以挂元素,但是这不是无限的,规定不超过八个
如果超过八个了怎么办
那就会创建一个新的桶来存放这些键,这个桶就叫 溢出桶。其实就是原来的桶放不下了,元素溢出到这个桶来了,创建完毕后,原来的哈希桶会有一个指针指向这个溢出桶,那原来的桶和溢出桶连起来就形成了一个 bmap 链表

超过八个创建溢出桶的情况:
在这里插入图片描述

实际上溢出的桶是:
在这里插入图片描述

如果哈希冲突很多很多,溢出桶也会越来越多,那么在读写哈希表时,就需要遍历更多的溢出桶链表,才能找到指定的位置,所以性能就越差。为了改善这种情况,应该增加buckets桶的数量,也就是 扩容
那什么时候进行扩容呢,总需要一个标准吧
所以引出了 负载因子 loadfactor

四、负载因子

负载因子 = 元素个数 ÷ 桶的个数,意思就是 实际的元素个数 / 哈希桶总个数

loadfactor := len(elems) / len(buckets)

负载因子越大,说明实际有的元素个数越多,也就是哈希冲突越大
负载因子越小,说明实际有的元素比较少,也就是说有更多的桶没有用到,所以 hmap 的内存利用率低,占用的内存就越大


扩容的条件:

  • 负载因子超过阈值 bucketCnt*13/16,也就是6.5 (bucketCnt 是每个哈希桶最多存储的键值对个数,也就是 8 )
  • 溢出桶数量过多

overLoadFactor 函数是 Go 语言中用于计算和检查 map 的负载因子的函数,判断负载因子是否超过阈值

func overLoadFactor(count int, B uint8) bool {return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

这段代码返回一个 bool 值,负载因子超过阈值返回 true,否则返回false

根据这段代码也可以看出来,在满足这两个条件下负载因子超过阈值:
是元素个数(count)大于桶内最多元素个数(bucketCnt),也就是语句
count > bucketCnt

是负载因子大于阈值,也就是语句uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
解释:其中 loadFactorNum 和 loadFactorDen 都是一个常数,bucketshift 是计算1 << B,并且已知loadFactorNum = (bucketCnt * 13 / 16) * loadFactorDen
,将 loadFactorNum 代入这个式子,最后化简得到uintptr(count) / 1 << B > (bucketCnt * 13 / 16)

其中(bucketCnt * 13 / 16)值为 6.5,1 << B就是哈希桶的数量,count 是元素个数


五、哈希函数

f32hash 函数是 Go 语言中用于计算 float32 类型数据的哈希值的函数,位于 runtime/alg.go 文件中,作用是根据 float32 数据的内存内容计算一个哈希值。在计算哈希时,它会根据浮点数的值处理 +0、-0 和 NaN 等特殊情况。最终会返回一个基于内存的哈希值。

func f32hash(p unsafe.Pointer, h uintptr) uintptr {f := *(*float32)(p)switch {case f == 0:return c1 * (c0 ^ h) // +0, -0case f != f:return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaNdefault:return memhash(p, h, 4)}
}

各个部分解释
1.

p unsafe.Pointer 和 h uintptr

p 是一个指向内存的指针,它指向 float32 类型的数据
h 是一个哈希种子,通常用于哈希计算的初始值,可能来自于现有的计算或随机值

f := *(*float32)(p):

这行代码将 unsafe.Pointer 转换为 float32 类型

  1. switch 语句的三个分支:

    前面提到了,它会根据浮点数的值处理 +0、-0 和 NaN 等特殊情况,最终会返回一个基于内存的哈希值。switch 语句就是做这个工作的

  • 处理 +0 和 -0
case f == 0:return c1 * (c0 ^ h) // +0, -0

f == 0 会处理 float32 类型的 +0 和 -0(它们在内存中是相同的)。为了确保它们产生不同的哈希值(避免冲突),这个条件会根据 h 和常数 c0、c1 计算哈希

  • 处理 NaN
case f != f:return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaN

f != f 是一个典型的用来检测 NaN(Not a Number,非数字)的技巧,因为 NaN 是唯一一个不等于自身的浮点数。这个分支处理 NaN 值的情况,并通过引入 fastrand()(一个生成随机数的函数)来增强哈希的随机性。

  • 默认情况(普通情况)
default:return memhash(p, h, 4)  // 普通情况,计算基于内存的哈希值

如果 f 既不是 0 也不是 NaN,那么就会调用 memhash 函数来计算该 float32 值的哈希。memhash 通过读取 f 在内存中的实际内容来计算哈希值。

  1. memhash 函数
    memhash 函数通常用于基于内存内容来计算哈希值。它的计算是基于内存块的字节内容,因此不同内存位置的相同数据可能会产生不同的哈希值。4 表示这里的内存块是 4 字节(即 float32 类型的大小)。
      

为什么要处理 0 ?

在浮点数中,+0 和 -0 是不同的值,但是它们在比较时是相等的。为了区分它们,Go 可能会采用不同的哈希策略
这里使用 c1 * (c0 ^ h) 的方式对它们进行哈希计算,防止它们在哈希表中发生冲突
  

为什么 map 哈希计算基于内存?
Go 中的 map 哈希计算并不仅仅依赖于数据的类型,还基于数据的内存布局。这是为了保证哈希计算的高效性和减少哈希冲突。通过直接操作内存地址和数据内容,可以更加高效地计算哈希值,减少对类型的依赖。尤其是在 map 中,键的哈希值越唯一,查找和插入操作的性能越高

为什么内存中的哈希值不能持久化?

  1. 内存不一致:

    哈希值是根据数据在内存中的表示来计算的。由于 Go 的内存布局可能在不同的运行中不同(例如由于编译器优化或操作系统调度等原因),内存地址的分布在不同的运行时是不一致的。因此,基于内存的哈希值不适合持久化,因为它在每次程序执行时都会有所变化。

  2. 运行时依赖:

    map 中的哈希值依赖于数据在内存中的位置或布局,这些信息只有在程序运行时才可用,无法在磁盘上持久保存。因此,每次程序运行时,哈希值的计算都会重新基于内存进行,而不能依赖于之前保存的哈希值。


在该文件中还有一个名为 typehash 的函数,它是一个通用的哈希计算方法,依据类型信息和数据本身来计算哈希值

func typehash(t *_type, p unsafe.Pointer, h uintptr) uintptr

解释:

t *_type : 这是类型信息,指向存储类型信息的结构体 _type。_type 是 Go 内部用于表示类型的结构体,包含了类型的详细信息(如类型大小、种类、结构体字段等)。

p unsafe.Pointer : 这是指向实际数据的指针。数据的类型会根据 t 来确定。

h uintptr : 这是哈希计算的种子,用于初始化哈希计算。

typehash 函数通过对类型信息 ( t ) 和数据本身 ( p ) 进行处理,返回一个最终的哈希值。由于不同类型的数据可能需要不同的哈希计算策略,typehash 会根据类型的不同来调整计算方法

通用性和速度
typehash 相比于其他类型特定的哈希函数(如 f32hash),要慢一些,因为它要处理不同类型的情况。比如它需要根据类型的结构、字段以及其他特征来计算哈希值,因此它的计算会更复杂一些,但却非常通用,能处理各种类型的数据

reflect_typehash 函数
在 Go 中,reflect_typehash 是一个使用 typehash 的辅助函数,主要用于通过反射机制来获取对象的哈希值。

//go:linkname reflect_typehash reflect.typehash
func reflect_typehash(t *_type, p unsafe.Pointer, h uintptr) uintptr {return typehash(t, p, h)
}

typehash 的实现会根据传入的数据类型来采取不同的计算策略。例如:

  • 基础类型(如整数、浮点数):直接对数据本身进行哈希计算。

  • 结构体类型:对结构体中的各个字段进行哈希计算,并合并所有字段的哈希值。

  • 数组、切片:对数组或切片中的元素进行哈希计算。

  • 接口类型:由于接口在 Go 中由类型和数据两个部分组成,typehash 需要同时考虑这两个部分来计算哈希值。

为什么 typehash 不用于 map 计算?
map 在 Go 中是一个特殊的类型,它具有高效的哈希计算过程。map 使用基于内存地址的哈希计算方法(如 memhash)来确保高效的哈希操作,特别是在内存分布和性能优化方面。相比之下,typehash 的计算方式更加通用,但也更慢。对于 map 来说,它并不需要处理如此复杂的类型,因此采用了更加高效、类型特定的哈希方法


六、扩容

前面已经提到了,扩容有两个条件:

  • 负载因子超过 6.5
  • 溢出桶的数量过多

判断负载因子是否超过阈值的函数是overLoadFactor,前面负载因子部分已经提到过
判断溢出桶的数量是否过多的函数是runtime.tooManyOverflowBuckets,代码如下

func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {if B > 15 {B = 15}return noverflow >= uint16(1)<<(B&15)
}

解释:

noverflow uint16 :无符号 16 位整数,表示当前哈希表中溢出桶的数量

B uint8:无符号 8 位整数,表示哈希表中哈希桶的数量的大小( 1 << B)

返回值:返回一个布尔值(bool),如果当前的溢出桶数量 noverflow 超过了预期的阈值,返回 true,否则返回 false

return noverflow >= uint16(1)<<(B&15):判断当前的溢出桶数量是否超出阈值

uint16(1) << (B & 15):B & 15:通过 B & 15,将 B 限制在一个 4 位二进制数范围内(即 0 到 15)。这等同于取 B 的低 4 位。如果 B 的值大于 15,这个操作会确保计算结果不会超出 2^15

判断是否需要扩容的代码如下

if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {hashGrow(t, h)goto again // Growing the table invalidates everything, so try again
}

!h.growing():检查哈希表是否没有处于扩容状态。如果 growing() 返回 false,说明当前哈希表没有扩容
可以看到的就是这三个条件限制

  • 当前不能正在扩容
  • 负载因子小于 6.5
  • 溢出桶数量不能过多

负责扩容的函数是runtime.hashGrow

func hashGrow(t *maptype, h *hmap)

根据触发扩容的条件不同,扩容的类型也不同,分为两种:

  • 增量扩容
  • 等量扩容

增量扩容

当负载因子很大时,哈希冲突比较严重,就会形成很多溢出桶
这时候再去查找一个元素,就要遍历更多的溢出桶链表,导致 map 读写性能下降

为什么溢出桶过多会导致 map 读写性能下降呢?
遍历的时间复杂度是O(n),哈希表查找的时间复杂度主要取决于哈希值的计算时间遍历的时间,若遍历的时间远小于计算哈希的时间,查找的时间复杂度近似为O(1) 。但如果哈希冲突比较严重,过多 key 都被分到同一个哈希桶,溢出桶链表过长导致遍历时间增大,就会导致查找的时间增大,而增删改查都需要先进性查找操作,因而导致性能下降

这种情况使用 增量扩容新增更多的哈希桶,避免形成过长的溢出桶链表

增量扩容意味着 map 在扩容时,并不是立即将所有元素迁移到新的桶中,而是逐步进行迁移,随着操作的进行,逐渐完成迁移。这样做的好处是可以减少扩容过程中对性能的影响,避免一次性扩容带来的性能瓶颈

增量扩容的核心机制

  • 老桶数组(oldbuckets):hmap 中的 oldBuckets 指向原来的哈希桶数组,表示这是旧数据

  • 创建新桶:创建更大容量的哈希桶数组,让 hmap 中的 buckets 指向新的哈希桶数组

  • 元素迁移:迁移的过程是增量的,即在每次对 map 进行操作(插入、删除等)时,都会将一些原本应该迁移的元素从 oldbuckets 迁移到新桶数组中。直到所有元素都完成迁移,扩容过程才算完成。扩容完成后,旧桶的内存被释放

  • 增量迁移的实现:这种增量迁移的方式通过一个标志 oldbuckets 来标识 map 是否正在进行扩容。如果 oldbuckets 不为 nil,则说明扩容正在进行中。如果为 nil,则说明扩容已经完成

等量扩容

前面提到过,等量扩容的触发条件是溢出桶数量过多

假如 map 先是添加了大量的元素,导致溢出桶增多,然后又大量删除元素,这样可能会导致数据分布不均匀,也就是有的桶有很多元素,有的桶元素很少甚至是空的,那也有可能导致很多的溢出桶都是空的,但是又占用了不少内存

这种情况使用 等量扩容创建一个同等容量的新 map,重新分配一次哈希桶

所以所谓等量扩容,实际上并不是扩大容量,buckets 数量不变,只是将所有元素二次分配使得数据分布更均匀

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

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

相关文章

Vue2 Extends 继承机制与组件复用实践

extends在某些场景下依然发挥作用&#xff0c;如Options API。子组件将继承父组件的属性、方法、生命周期钩子函数以及混合&#xff08;mixins&#xff09;等选项。 注意&#xff1a;子组件可以覆盖、或继承扩展父组件的选项。子组件的生命周期钩子和父组件的钩子一起执行。 &l…

openSUSE MicroOS不可变Linux

openSUSE MicroOS不可Linux 1、openSUSE MicroOS简介安装时可能遇到的问题 2、ssh登录3、openSUSE MicroOS配置国内软件源4、系统变更openSUSE MicroOS安装软件包方法1&#xff1a;进入事务性更新模式安装软件包方法2&#xff1a;继续快照id基于这个快照进行增量安装方法3&…

建站SEO优化之站点地图sitemap

文章目录 编写规范小型网站站点地图小型网站规范示例站点地图说明 大型网站站点地图大型网站规范示例以豆瓣站点地图为例 近期文章&#xff1a; 个人建站做SEO网站外链这一点需要注意&#xff0c;做错了可能受到Google惩罚一文搞懂SEO优化之站点robots.txt网页常见水印实现方式…

Java分层开发必知:PO、BO、DTO、VO、POJO概念详解

目录 引言一、核心概念与定义1、PO&#xff08;Persistent Object&#xff0c;持久化对象&#xff09;2、BO&#xff08;Business Object&#xff0c;业务对象&#xff09;3、DTO&#xff08;Data Transfer Object&#xff0c;数据传输对象&#xff09;4、VO&#xff08;View O…

Linux下OLLAMA安装卡住怎么办?

网络环境不理想&#xff0c;经常在官方的linux安装脚本执行时卡住&#xff0c;其实主要是下载文件卡住&#xff0c;于是我想到了是否可以把其中下载的过程显化、分步&#xff0c;这样更可控&#xff0c;于是修改了官方的install.sh #!/bin/sh # This script installs Ollama o…

C++面试(5)-----删除链表中指定值的节点

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 给定一个单向链表的头节点 head 和一个特定值 val&#xff0c;要求编写一个函数来删除链表中所有值等于 val 的节点&#xff0c;并返回修改后的链表头节点。 示例&#xff1a; 输…

如何用AI赋能学习

由于博主是大学生&#xff0c;今天花费了大量的时间去进行期末的复习&#xff0c;不过从复习中得到了一些学习的灵感&#xff0c;即&#xff1a;如何用AI赋能学习 当我们需要掌握一门新的技能的时候&#xff0c;我们很容易的想到三种办法&#xff1a;买书自己学&#xff0c;报…

【threejs】每天一个小案例讲解:常见材质

代码仓 GitHub - TiffanyHoo/three_practices: Learning three.js together! 可自行clone&#xff0c;无需安装依赖&#xff0c;直接liver-server运行/直接打开chapter01中的html文件 运行效果图 知识要点 1. MeshBasicMaterial&#xff08;基础网格材质&#xff09; • 特…

springboot后端与鸿蒙的结合

软件&#xff1a;鸿蒙devceo3.1&#xff0c;springboot项目采用IDEA 目的&#xff1a; 1、结合springboot后端与鸿蒙的结合运用。 2、Log日志查看console语句的信息。 3、引入 import http from ohos.net.http。 4、调用springboot后端提供的链接发送post 5、TextInput的…

minio集群通过mc mirror命令进行定时备份,支持X86和arm两种架构

文章目录 前言一、思路二、使用步骤1.下载mc二进制文件2.手动测试备份命令3.配置定时任务4.成功截图 总结 前言 通过mc mirror命令对minio集群进行定时备份。 一、思路 通过mc mirror命令配合crond定时任务进行周期性的备份 二、使用步骤 1.下载mc二进制文件 wget https:…

三大能力升级,为老项目重构开辟新路径

在软件技术飞速迭代的今天&#xff0c;老项目重构是开发者们绕不开的难题。接口实现缺失、业务逻辑矛盾、架构规划偏离等问题如同拦路虎&#xff0c;让重构工作举步维艰。而传统的 AI 辅助方式&#xff0c;因未充分关联项目实际情况&#xff0c;犹如 “空中造楼”&#xff0c;难…

AES加密

AES加密算法详解 AES&#xff08;Advanced Encryption Standard&#xff09;是一种对称密钥分组加密算法&#xff0c;用于保护电子数据的安全性。其核心特点是通过相同的密钥进行加密和解密&#xff0c;属于对称加密体系。。以下从核心特性、加密流程及安全性三方面展开说明&a…

关于联咏(Novatek )自动曝光中Lv值的计算方式实现猜想

目录 一、常见Lv对应的实际场景 二、常见光圈值 三、最小二乘法计算SV中的系数K

[docker]镜像操作:关于docker pull、save、load一些疑惑解答

在使用 Docker 的过程中&#xff0c;镜像管理是极其重要的一环。无论是拉取、保存还是加载镜像&#xff0c;每一个步骤都可能遇到一些疑问或者误区。 本文将结合实际案例&#xff0c;对常见的 Docker 镜像操作问题进行系统性总结&#xff0c;帮你更好地理解 Docker 镜像的工作机…

SFTrack:面向警务无人机的自适应多目标跟踪算法——突破小尺度高速运动目标的追踪瓶颈

【导读】 本文针对无人机&#xff08;UAV&#xff09;视频中目标尺寸小、运动快导致的多目标跟踪难题&#xff0c;提出一种更简单高效的方法。核心创新在于从低置信度检测启动跟踪&#xff08;贴合无人机场景特性&#xff09;&#xff0c;并改进传统外观匹配算法以关联此类检测…

什么是渗透测试,对网站安全有哪些帮助?

在网络安全的战场中&#xff0c;网站如同暴露在数字世界的堡垒&#xff0c;时刻面临着黑客攻击的威胁。而渗透测试&#xff0c;就像是为网站进行一场 “模拟攻防演练”&#xff0c;它以黑客的思维和手段&#xff0c;主动出击&#xff0c;探寻网站潜在的安全漏洞。究竟什么是渗透…

KU115LPE-V10型FPGA加速卡

KU115LPE-V10是一款基于PCI Express总线通信的FPGA加速类产品。 该产品基于Xilinx公司的的高性能Kintex Ultra-Scale FPGA设计&#xff0c;配置最大两组DDR4缓存单元&#xff0c;每组最大支持4GB容量&#xff0c;72bit&#xff08;包含ECC&#xff0c;8bit&#xff09;&#x…

【笔记】Blockchain

区块链Blockchain是一种分布式数据库技术&#xff0c;其核心特点在于去中心化、不可篡改和透明性。它通过一系列按照时间顺序排列的数据块&#xff08;即“区块”&#xff09;组成&#xff0c;每个数据块都包含了一定时间内的一系列信息交易&#xff0c;并通过密码学方法确保这…

GitHub Desktop Failure when receiving data from the peer

目录 安装Github Desktop简易省流助手 解决 Git 克隆时出现的 "Failure when receiving data from the peer" 错误1. 网络连接问题原因&#xff1a;解决办法&#xff1a; 2. Git 配置问题原因&#xff1a;解决办法&#xff1a; 3. GitHub 服务故障原因&#xff1a;解…

疏锦行Python打卡 DAY 27 函数专题2:装饰器

def logger(func):def wrapper(*args, **kwargs):print(f"开始执行函数 {func.__name__}&#xff0c;参数: {args}, {kwargs}")result func(*args, **kwargs)print(f"函数 {func.__name__} 执行完毕&#xff0c;返回值: {result}")return resultreturn wr…