当前位置: 首页 > 面试题库 >

引擎盖下的地图

姜煌
2023-03-14
问题内容

在阅读了戴夫·切尼(Dave Cheney)关于Go的地图的博客文章之后,对我来说,还有几件事尚不清楚。

TLDR:

  • 为什么它们无序?
  • 实际值存储在哪里?

深入研究运行时程序包后,我发现基本的映射结构如下:

// A header for a Go map.
type hmap struct {
    // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/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     uint8
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    hash0     uint32 // hash seed

    buckets    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 growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

buckets -是存储区数组,其中索引是键的哈希值的低位,其中存储区为:

// A bucket for a Go map.
type bmap struct {
    // tophash generally contains the top byte of the hash value
    // for each key in this bucket. If tophash[0] < minTopHash,
    // tophash[0] is a bucket evacuation state instead.
    tophash [bucketCnt]uint8
    // Followed by bucketCnt keys and then bucketCnt elems.
    // NOTE: packing all the keys together and then all the elems together makes the
    // code a bit more complicated than alternating key/elem/key/elem/... but it allows
    // us to eliminate padding which would be needed for, e.g., map[int64]int8.
    // Followed by an overflow pointer.
}

..好吧,这只是uint8每个项目是键的哈希值的第一个字节的数组。键值对存储为key/key value/value(每个存储桶八对)。但是到底在哪里?考虑到映射可能包含(几乎)任何类型的值。应该有某种指针可以放置在存储值数组的内存中,但bmap没有此类信息。

而且由于键的哈希值位于存储桶中的有序数组中,为什么每次我遍历地图时它的顺序都不同?


问题答案:
  • 为什么它们无序?

因为这为运行时提供了 更大的自由
以实现映射类型。尽管我们知道Go的(当前)实现是一个哈希图,但语言规范允许使用任何地图实现(例如哈希图,树图等)。也不必记住顺序,这使运行时可以更有效地完成工作,减少使用记忆。

Adrian的评论很好地总结了很少需要订单,而始终保持订单是浪费。当您确实需要订购时,可以使用提供订购的数据结构。

而且由于键的哈希值位于存储桶中的有序数组中,为什么每次我遍历地图时它的顺序都不同?

Go的作者有意使地图的迭代顺序随机化(因此我们凡人不会依赖固定的顺序)。

  • 实际值存储在哪里?

“ where”由指定hmap.buckets。这是一个指针值,它指向内存中的一个数组,该数组保存存储桶。

buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.

因此,hmap.buckets指向存储桶的连续内存段。存储桶由进行“建模”
bmap,但这不是其实际的内存布局。存储桶的开头是一个存储桶(tophash [bucketCnt]uint8)中存储键的最高哈希字节的数组,该数组
后面bucketCnt是存储桶的键,然后bucketCnt是存储桶的值。最后,有一个溢出指针。

将存储桶想像成这种 概念性 类型,它可以“可视化”键和值在内存中的位置:

type conceptualBucket struct {
    tophash     [bucketCnt]uint8
    keys        [bucketCnt]keyType
    values      [bucketCnt]valueType
    overflowPtr uintptr
}

注意:bucketCnt是一个编译时间常数,为8,它是存储桶可以容纳的密钥/元素对的最大数量。

当然,这种“图片”是不准确的,但是它给出了存储键和值的位置/方式的想法。



 类似资料:
  • 我必须评估Codename One,但我找不到关于部署在底层是如何工作的以及最终结果是什么的信息。他们是否将我的Java代码交叉编译为类似于RoboVM的真实本机代码,他们是否使用类似于Gluon的JVM,或者他们有自己的JVM?

  • 尝试在中使用分组,并且没有任何原因得到这些绑定错误(它们不属于我的代码,我也看不到处理它们的方法): System. Windows. Data错误:4:找不到引用“RelativeSource FindAncestor, AncestorType=”System. Windows. Control. DataGrid', AncestorLine=“1”的绑定源。Binding表达式:路径=区域

  • 除了阅读github中的代码之外,是否有关于signalr.redis包如何工作的白皮书类型的文档?具体地说,我想知道它为Redis添加了哪些键、更新/删除策略等。当查看Redis内部时,我只看到以下调用中指定的一个键(即“signalr.Redis.sample”): 这把钥匙好像是Redis的柜台。我假设正在创建其他键并迅速删除,以方便连接到Redis的每个应用服务器之间的消息。

  • 问题内容: 我一直在尝试React Hooks,它们似乎确实简化了诸如存储状态之类的事情。但是,他们似乎通过魔术来做很多事情,而我找不到关于它们实际工作方式的好文章。 看起来很神奇的第一件事是,每次调用setXXX方法时,调用诸如useState()之类的函数如何导致功能组件的重新渲染? 当功能组件甚至没有能力在Mount / Unmount上运行代码时,诸如useEffect()之类的东西如何伪

  • Java的switch语句是如何工作的?它如何将所使用变量的值与案例部分中给出的值进行比较?它是否使用或,还是完全是其他原因? 我主要对1.7之前的版本感兴趣。

  • 我一直在尝试React钩子,它们似乎可以简化存储状态之类的事情。然而,他们似乎用魔法做了很多事情,我找不到一篇关于他们如何实际工作的好文章。 第一件看起来很神奇的事情是,调用像useState()这样的函数是如何在每次调用setXXX方法时导致函数组件的重新渲染的? 当功能组件甚至不具备在挂载/卸载上运行代码的能力时,像use效应()这样的东西是如何伪造组件的? useContext()实际上是如