1.golang map 源码解读(8问)
2.三万字带你认识 Go 底层 map 的码解实现
3.结合源码探究HashMap初始化容量问题
4.MapReduce源码解析之Mapper
5.concurrenthashmap1.8源码如何详细解析?
6.map在golang的底层实现和源码分析
golang map 源码解读(8问)
map底层数据结构为hmap,包含以下几个关键部分:
1. buckets - 指向桶数组的码解指针,存储键值对。码解
2. count - 记录key的码解数量。
3. B - 桶的码解数量的对数值,用于计算增量扩容。码解网页展示源码
4. noverflow - 溢出桶的码解数量,用于等量扩容。码解
5. hash0 - hash随机值,码解增加hash值的码解随机性,减少碰撞。码解
6. oldbuckets - 扩容过程中的码解旧桶指针,判断桶是码解否在扩容中。
7. nevacuate - 扩容进度值,码解小于此值的码解已经完成扩容。
8. flags - 标记位,用于迭代或写操作时检测并发场景。
每个桶数据结构bmap包含8个key和8个value,以及8个tophash值,用于第一次比对。
overflow指向下一个桶,桶与桶形成链表存储key-value。
结构示意图在此。
map的初始化分为3种,具体调用的函数根据map的初始长度确定:
1. makemap_small - 当长度不大于8时,只创建hmap,不初始化buckets。
2. makemap - 当长度参数为int时,底层调用makemap。
3. makemap - 初始化hash0,计算对数B,并初始化buckets。
map查询底层调用mapaccess1或mapaccess2,前者无key是否存在的bool值,后者有。
查询过程:计算key的hash值,与低B位取&确定桶位置,获取tophash值,比对tophash,相同则比对key,获得value,否则继续寻找,直至返回0值。
map新增调用mapassign,步骤包括计算hash值,确定桶位置,比对tophash和key值,插入元素。
map的扩容有两种情况:当count/B大于6.5时进行增量扩容,容量翻倍,渐进式完成,每次最多2个bucket;当count/B小于6.5且noverflow大于时进行等量扩容,容量不变,但分配新bucket数组。
map删除元素通过mapdelete实现,好客系统源码查找key,计算hash,找到桶,遍历元素比对tophash和key,找到后置key,value为nil,修改tophash为1。
map遍历是无序的,依赖mapiterinit和mapiternext,选择一个bucket和offset进行随机遍历。
在迭代过程中,可以通过修改元素的key,value为nil,设置tophash为1来删除元素,不会影响遍历的顺序。
三万字带你认识 Go 底层 map 的实现
map在Go语言中是一种基础数据结构,广泛应用于日常开发。其设计遵循“数组+链表”的通用思路,但Go语言在具体实现上有着独特的设计。本文将带你深入了解Go语言中map的底层实现,包括数据结构设计、性能优化策略以及关键操作的内部实现。
在Go语言的map中,数据存储在数组形式的桶(bucket)中,每个桶最多容纳8对键值对。哈希值的低位用于选择桶,而高位则用于在独立的桶中区分键。这种设计有助于高效地处理冲突和实现快速访问。
源码位于src/runtime/map.go,展示了map的内部结构和操作。在该文件中,定义了桶和map的内存模型,桶的内存结构示例如下。每个桶的前7-8位未被使用,用于存储键值对,避免了不必要的内存填充。在桶的末尾,还有一个overflow指针,用于连接超过桶容量的键值对,以构建额外的桶。
初始化map有两种方式,根据是否指定初始化大小和hint值,调用不同的函数进行分配。对于不指定大小或hint值小于8的情况,使用make_small函数直接在堆上分配。当hint值大于8时,调用makemap函数进行初始化。
插入操作的核心是找到目标键值对的内存地址,并通过该地址进行赋值。在实现中,没有直接将值写入内存,而是返回值在内存中的对应地址,以便后续进行赋值操作。同时,当桶达到容量上限时,会创建新的直尺公式源码溢出桶来容纳多余的数据。
查询操作通过遍历桶来实现,找到对应的键值对。对于查询逻辑的优化,Go语言提供了不同的函数实现,如mapaccess1、mapaccess2和mapaccessK等,它们在不同场景下提供高效的关键字查找和值获取。
当map需要扩容时,Go语言会根据装载因子进行决策,以保持性能和内存使用之间的平衡。扩容操作涉及到数据搬移,通过hashGrow()和growWork()函数实现。增量扩容增加桶的数量,而等量扩容则通过重新排列元素提高桶的利用率。
删除操作在Go语言中同样高效,利用map的内部机制快速完成。迭代map时,可以使用特定的函数遍历键值对,实现对数据的访问和操作。
通过深入分析Go语言中map的实现,我们可以看到Go开发者在设计时的巧妙和全面考虑,不仅关注内存效率,还考虑到数据结构在不同情况下的复用和性能优化。这种设计思想不仅体现在map自身,也对后续的缓存库等开发产生了深远的影响。
综上所述,Go语言中map的底层实现展示了高效、灵活和强大的设计原则,为开发者提供了强大的工具,同时也启发了其他数据结构和库的设计。了解这些细节有助于我们更深入地掌握Go语言的特性,并在实际开发中做出更优的选择。
结合源码探究HashMap初始化容量问题
探究HashMap初始化容量问题
在深入研究HashMap源码时,有一个问题引人深思:为何在知道需要存储n个键值对时,我们通常会选择初始化容量为capacity = n / 0. + 1?
本文旨在解答这一疑惑,适合具备一定HashMap基础知识的读者。请在阅读前,思考以下问题:
让我们通过解答这些问题,逐步展开对HashMap初始化容量的深入探讨。
源码探究
让我们从实际代码出发,通过debug逐步解析HashMap的初始化逻辑。
举例:初始化一个容量为9的HashMap。
执行代码后,我们发现初始化容量为,且阈值threshold设置为。
解析
通过debug,我们首先关注到构造方法中的初始化逻辑。注意到,初始化阈值时,实际调用的是`tabliSizeFor(int n)`方法,它返回第一个大于等于n的2的幂。例如,`tabliSizeFor(9)`返回,`tabliSizeFor()`返回,汇率支付源码`tabliSizeFor(8)`返回8。
继续解析
在构造方法结束后,我们通过debug继续追踪至`put`方法,直至`putVal`方法。
在`putVal`方法中,我们发现当第一次调用`put`时,table为null,从而触发初始化逻辑。在初始化过程中,关键在于`resize()`方法中对新容量`newCap`的初始化,即等于构造方法中设置的阈值`threshold`()。
阈值更新
在初始化后,我们进一步关注`updateNewThr`的代码逻辑,发现新的阈值被更新为新容量乘以负载因子,即 * 0.。
案例分析
举例:初始化一个容量为8的HashMap。
解答:答案是8,因为`tableSizeFor`方法返回大于等于参数的2的幂,而非严格大于。
扩容问题
举例:当初始化容量为时,放入9个不同的entry是否会引发扩容。
解答:不会,因为扩容条件与阈值有关,当map中存储的键值对数量大于阈值时才触发扩容。根据第一问,初始化容量是,阈值为 * 0. = 9,我们只放了9个,因此不会引起扩容。
容量选择
举例:已知需要存储个键值对,如何选择合适的初始化容量。
解答:初始化容量的目的是减少扩容次数以提高效率并节省空间。选择容量时,应考虑既能防止频繁扩容又能充分利用空间。具体选择取决于实际需求和预期键值对的数量。
总结
通过本文的探讨,我们深入了解了HashMap初始化容量背后的逻辑和原因。希望这些解析能够帮助您更深入地理解HashMap的内部工作原理。如果您对此有任何疑问或不同的见解,欢迎在评论区讨论。
最后,如有帮助,欢迎点赞分享。
MapReduce源码解析之Mapper
MapReduce,大数据领域的标志性计算模型,由Google公司研发,其核心概念"Map"与"Reduce"简明易懂却威力巨大,打开了大数据时代的大门。对于许多大数据工作者来说,MapReduce是基础技能之一,而源码解析更是深入理解与实践的必要途径。 MapReduce由两部分组成:Map与Reduce。Map阶段通过映射函数将一组键值对转换成另一组键值对,而Reduce阶段则负责合并这些新的兔子指标源码键值对。这种并行计算模型极大地提高了大数据处理的效率。 本文将聚焦于Map阶段的核心实现——Mapper。通过解析Mapper类及其子类的源码,我们可以更深入地理解MapReduce的工作机制,并在易观千帆等技术数据处理中发挥更大的效能。 Mapper类内部包含四个关键方法与一个抽象类: setup():主要为map()方法做准备,例如加载配置文件、传递参数。 cleanup():用于清理资源,如关闭文件、处理Key-Value。 map():程序的逻辑核心,对输入的文本进行处理(如分割、过滤),以键值对的形式写入context。 run():驱动Mapper执行的主方法,按照预设顺序执行setup()、map()、cleanup()。 Context抽象类扮演着重要角色,用于跟踪任务状态和数据存储,如在setup()中读取配置信息,并作为Key-Value载体。 下面是几个Mapper子类的详细解析: InverseMapper:将键值对反转,适用于不同需求的统计分析。 TokenCounterMapper:使用StringTokenizer对文本进行分割,计算特定token的数量,适用于词频统计等。 RegexMapper:对文本进行正则化处理,适用于特定格式文本的统计。 MultithreadedMapper:利用多线程执行Mapper任务,提高CPU利用率,适用于并发处理。 本文对MapReduce中Mapper及其子类的源码进行了详尽解析,旨在帮助开发者更深入地理解MapReduce的实现机制。后续将探讨更多关键类源码,以期为大数据处理提供更深入的洞察与实践指导。concurrenthashmap1.8源码如何详细解析?
ConcurrentHashMap在JDK1.8的线程安全机制基于CAS+synchronized实现,而非早期版本的分段锁。
在JDK1.7版本中,ConcurrentHashMap采用分段锁机制,包含一个Segment数组,每个Segment继承自ReentrantLock,并包含HashEntry数组,每个HashEntry相当于链表节点,用于存储key、value。默认支持个线程并发,每个Segment独立,互不影响。
对于put流程,与普通HashMap相似,首先定位至特定的Segment,然后使用ReentrantLock进行操作,后续过程与HashMap基本相同。
get流程简单,通过hash值定位至segment,再遍历链表找到对应元素。需要注意的是,value是volatile的,因此get操作无需加锁。
在JDK1.8版本中,线程安全的关键在于优化了put流程。首先计算hash值,遍历node数组。若位置为空,则通过CAS+自旋方式初始化。
若数组位置为空,尝试使用CAS自旋写入数据;若hash值为MOVED,表示需执行扩容操作;若满足上述条件均不成立,则使用synchronized块写入数据,同时判断链表或转换为红黑树进行插入。链表操作与HashMap相同,链表长度超过8时转换为红黑树。
get查询流程与HashMap基本一致,通过key计算位置,若table对应位置的key相同则返回结果;如为红黑树结构,则按照红黑树规则获取;否则遍历链表获取数据。
map在golang的底层实现和源码分析
在Golang 1..2版本中,map的底层实现由两个核心结构体——hmap和bmap(此处用桶来描述)——构建。初始化map,如`make(map[k]v, hint)`,会创建一个hmap实例,包含map的所有信息。makemap函数负责创建hmap、计算B值和初始化桶数组。
Golang map的高效得益于其巧妙的设计:首先,key的hash值的后B位作为桶索引;其次,key的hash值的前8位决定桶内结构体的数组索引,包括tophash、key和value;tophash数组还用于存储标志位,当桶内元素为空时,标志位能快速识别。读写删除操作充分利用了这些设计,包括更新、新增和删除key-value对。
删除操作涉及到定位key,移除地址空间,更新桶内tophash的标志位。而写操作,虽然mapassign函数返回value地址但不直接写值,实际由编译器生成的汇编指令提高效率。扩容和迁移机制如sameSizeGrow和biggerSizeGrow,针对桶利用率低或桶数组满的情况,通过调整桶结构和数组长度,优化查找效率。
evacuate函数负责迁移数据到新的桶区域,并清理旧空间。最后,虽然本文未详述,但订阅"后端云"公众号可获取更多关于Golang map底层实现的深入内容。
MapReduce源码解析之InputFormat
导读
深入探讨MapReduce框架的核心组件——InputFormat。此组件在处理多样化数据类型时,扮演着数据格式化和分片的角色。通过设置job.setInputFormatClass(TextInputFormat.class)等操作,程序能正确处理不同文件类型。InputFormat类作为抽象基础,定义了文件切分逻辑和RecordReader接口,用于读取分片数据。本节将解析InputFormat、InputSplit、RecordReader的结构与实现,以及如何在Map任务中应用此框架。
类图与源码解析
InputFormat类提供了两个关键抽象方法:getSplits()和createRecordReader()。getSplits()负责规划文件切分策略,定义逻辑上的分片,而RecordReader则从这些分片中读取数据。
InputSplit类承载了切分逻辑,表示了给定Mapper处理的逻辑数据块,包含所有K-V对的集合。
RecordReader类实现了数据读取流程,其子类如LineRecordReader,提供行数据读取功能,将输入流中的数据按行拆分,赋值为Key和Value。
具体实现与操作流程
在getSplits()方法中,FileInputFormat类负责将输入文件按照指定策略切分成多个InputSplit。
TextInputFormat类的createRecordReader()方法创建了LineRecordReader实例,用于读取文件中的每一行数据,形成K-V对。
Mapper任务执行时,通过调用RecordReader的nextKeyValue()方法,读取文件的每一行,完成数据处理。
在Map任务的run()方法中,MapContextImp类实例化了一个RecordReader,用于实现数据的迭代和处理。
总结
本文详细阐述了MapReduce框架中InputFormat的实现原理及其相关组件,包括类图、源码解析、具体实现与操作流程。后续文章将继续探讨MapReduce框架的其他关键组件源码解析,为开发者提供深入理解MapReduce的构建和优化方法。
充分理解Linux GCC 链接生成的Map文件
简单来说,map文件就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件,里面包含函数大小,入口地址等一些重要信息。从map文件我们可以了解到:
生成map文件是链接器ld的功能,有两种方式可以生成map文件:
使用GNU binutils,必须通过设置正确的标志来显式地请求生成映Map文件。使用LD将Map打印到输出到output.map:
作为一个简单程序的例子,你可以使用以下命令链接编译单元:
为什么要了解Map文件:
在本文中,我想突出说明链接器Map文件是多么简单,以及它可以教给我们关于正在处理的程序的一些知识。
固件工程师很少在调试时使用构建过程生成的Map文件。然而,答案有时就在这个Map文件中。
Map文件提供了有价值的信息,可以帮助您理解和优化内存。我强烈建议为在生产环境中运行的任何固件保存该文件。
Map文件是整个程序的符号表。让我们深入研究它,看看它有多简单,以及如何有效地使用它。我将尝试用一些例子来说明,这些例子都是用GNU binutils来描述的。
LED闪烁程序:
还有什么比我们的老朋友LED闪烁程序示例解释一下Map文件的基础知识更好的呢?
为了了解Map文件,我们使用Nordic SDK中的LED闪烁程序来编译,并修改它以添加对atoi的调用。然后,我们将使用Map文件来分析这两个程序之间的差异。
下面就是示例的main.c文件:
编译:
生成的Map文件有多行 ,尽管它只是在闪烁发光二极管。这么多行不可能看不见,里面一定有一些重要的信息……
现在让我们修改程序,添加对atoi的调用,我们不直接使用整数作为延迟函数的参数,而是将其编码为字符串并使用atoi解码,然后作为参数传给延时函数。
经过编译,整个程序从字节变成了字节。
我们能够想到调用atoi会带来更多的代码,但是%程序大小的增加是巨大的!
深入研究Map文件:
在下面的部分中,我将使用代码片段来解释Map文件的不同部分。
Archives linked:
下面是Map文件的第一行内容:
上述信息的格式如下:
上面内容的意思是crt0这个文件中会调用exit函数,exit函数在exit.o这个目标文件中,exit.o目标文件是被链接在libc_nano.a这个库文件里的。
为什么是这样,不在本文的讨论范围内,但是你的工具链(这里是GNU工具链)确实提供了一些标准库。它们可用于提供atoi等标准功能,在这个例子中,我指定链接器使用nano.spec文件,这就是为什么标准函数都来自libc_nano.a。
现在,比较两个生成的map文件,发现的第一个区别是程序中包含了一些其他的存档成员:atoi,它本身需要_strtol_r,_strtol_r本身又需要_ctype_:
现在,我们对实际包含在程序中的文件以及它们存在的原因有了更好的理解。让我们来看看这个文件里面还有什么!
Memory configuration:
Map文件中最直接的信息是实际的内存区域,这些区域具有位置、大小和访问权限:
Linker script and memory map:
内存配置之后是Linker script and memory map,这个很有趣,因为它给出了程序中符号的详细信息。在我们的例子中,它首先指示text区域的大小及其内容(text是我们编译的代码,而data是程序数据)。
在这里,中断向量(在.isr_vector下)出现在可执行文件的开头,定义在gcc_startup_nrf.S中:
这些行给出了每个函数的地址和大小。在上面,你可以读取bsp_board_led_invert的地址,它来自boards.c.o(如你所猜测的,board.c的编译单元),在text区域中它的大小为0x字节。这样,我们就可以定位程序中使用的每个函数。
我的常量字符串_delay_ms_str在程序初始化时显然包含在程序中,只读数据作为rodata保存在链接器脚本中指定的FLASH区域中(存储在Flash中,而不是复制在RAM中,因为它是常量)。我可以在这行下面找到:
我还注意到_ctype_的包含在text区域中增加了0x字节的只读数据
标准库是开源的( 链接),我们很容易就能找出它占用那么多空间的原因。我深入到atoi的内部(可重入版本的atoi_r,见下文),它是直接调用的strtol_r:
对于strtol_r,它实际上比仅仅将字符转换为整数更复杂,因为还使用ctype来执行类型检查。ctype的工作方式是使用一个表,其中ASCII符号类型存储在一个数组中。下面是ctype的主要部分,并附上我的注释:
有趣的是,atoi的添加不仅增加了代码的大小(text区域),还增加了数据的大小(data区域)。分析两个Map文件,我可以很容易地发现之前被链接器丢弃的数据:
现在你可能已经注意到函数名以_r结尾,例如在调用strtol_r时,该后缀表示可重入性。有关可重入性的文档可以在 newlib源代码中找到。总而言之,即使同一函数已经在另一个进程中执行,也可以调用可重入函数,而不会干涉执行。从文档中可以看到如下描述:
Each function which uses the global reentrancy structure uses the global variable _impure_ptr, which points to a reentrancy structure.
在我们的例子中,我们需要新的全局变量来调用可重入函数:atoi_r。
最后要记住的一点信息是:初始化变量必须保存在Flash中,但它们在Map文件中会出现在RAM中,因为它们在进入主函数之前被复制到RAM中。在这里,符号__data_start__和__data_end__跟踪RAM中用于保存初始化变量的区域,这些值存储在Flash中,起始位置为0xd0:
Discarded sections:
如果链接器没有找到对函数和变量的任何引用,编译后包含在程序中的函数和变量并不总是最终二进制文件的一部分,它们将会被删除但是仍然会出现在Map文件的Discarded input sections 部分。例如,下面是一些定义在boards.c中的函数,它们永远不会被调用并因此被丢弃:
Common symbols:
这个部分没有出现在我们的Map文件中,但它值得一提。
Common symbols(通用符号)是可以在代码中的任何地方使用的非常量全局变量(non-constant global variables)。您可能知道,使用全局变量通常不是一个好的实践,因为它们使代码更难维护。确实如此,作用域是全局的,每个外部模块可以修改任何全局变量的值,访问时必须考虑到这一点。将变量隔离到一个模块中,使用static关键字,通常更好地确保创建变量的模块完全负责其状态。
现在,如果您希望使程序更安全并防止访问某些全局变量,请查看Map文件部分。如果某些变量不需要声明为全局变量,您可能希望将它们转换为静态变量。
Map文件有几种可能的用法:大多数时候,一个地址后面对应着一个函数,我们希望通过这个地址去了解一些问题。例如,它可以是硬故障处理程序(Hard Fault handler)中的程序计数器(Program Counter)。其他时候,你也会遇到调试一些不明确的行为,最终发现你的程序意外地写入了一个出界数组(数组越界)。当有了ELF文件时,arm-none-eabi-nm对于这些事情也非常有用,它提供了按大小排序符号的选项。
但有些时候,它甚至在你有可执行文件之前就很有用了……
Debugging a linking error:
Map文件是在构建代码(.o文件)链接在一起的时候生成的,这意味着它可以有助于解决链接过程中出现的错误。我记得在几个Flash页面中包含一个引导加载程序,在某些情况下,我想使用atoi,但引导加载程序不再编译,因为没有更多的可用空间。
使用前面的示例,假设我现在只有0x字节的Flash。编译第一个示例时,如果没有atoi,就不会出现问题,但是第二个例子会溢出我们的Flash:
是不是很讨厌?atoi只是一个很简单的函数而已,居然就出现这种问题。但正如我们前面所提到的,使用libc_nano.a需要比预期更多的Flash空间。
让我们来实现自己版本的atoi,其实也没那么难。以下是编译后的结果(config CUSTOM_ATOI):
这个方法是不是很好?现在可以将代码塞进0x字节的Flash,以满足我们的(假)需求。
分析Map文件可以让我们了解很多正在编写的代码,这是改进固件的第一步。
可以使用一些工具来解析Map文件并获得程序的汇总视图,后面有时间和大家好好聊聊。