Go的内存分配

我们都知道,golang之所以相比c++好用、开发效率高,得益于goroutine与channel的组合,使得开发并行程序的效率大大提升。但是其实开发并行程序,内存管理是相当的头疼,GC机制的加入大大降低了编写并行程序的门槛。

Go的内存分配的起点很高,思路继承于tcmalloc库,该库在并行程序下分配小对象(<=32K)的效率很高,一般各个程序替换为tcmalloc后,都会得到不少的性能提升。

tcmalloc的核心思想是把内存分为多级来降低锁的粒度。每个线程都会有一个cache,用于无锁分配小对象。当小对象分配完毕后,再向central来申请,当central都没有了,那直接向heap申请,heap最终会向操作系统来申请内存。

在golang中,差不多也是这个模式,要理解go的内存分配策略,首先得有以下几个概念:

  • Page。page和操作系统的page不一样,page是go的基本内存分配单位,但是大小不是必须和操作系统的page一样,一般是倍数于操作系统的page,一般为8K。
  • Span。连续的page组成的区域,用于分割成特定大小的区域,用于分配小对象。
  • Central。用于分配已经切割好的对应的空闲span。
  • Heap。用于大对象的分配,或者分配未切割的span,用于给central切割成小对象。

每一个P都有一个cache,用于无锁小对象分配。分配的小对象,分别以8字节的倍数为上界向上取整,来对所分配的对象尺寸进行分类。比如分配5字节,最终会去8字节的分配池中进行获取。以8字节定界,可以有效的降低需要管理的尺寸类型,也叫size class。每个P的cache中,都有一个定长数组,用于索引span。而寻址的下标是根据size class和8的结果来的,也就是确定了size class,我们可以直接寻址到对应用于分配相应size class的span,用于分配小对象。

所以任何G需要分配小对象内存的时候,根据对象的size class来确定对应的span,从span中取可用的小对象,这部操作效率是非常高的,G是从P中的cache取span的,而P只能关联到一个M,故这部操作是不用加锁的。

当P中的cache中对应size class的span中的小对象分配完了之后,需要从central中获取可用size class的span,由于多个P可能会同时操作central,这部操作是需要加锁的。和P中的cache一样,central也被分为了对应尺寸的定长数组,用于快速获取某个size class的span。由于此部消耗很高,所以从对象上来说,属于一次性取了一批用来进行小对象的分配。

当central中的span耗尽,那么就会从heap中申请,该申请也需要加锁。heap负责分配span,而分配入central中的span则会对应的进行小对象切分,给cache使用。heap最终内存还是从操作系统中进行申请的。

在堆中,同样的,使用定长数组来管理,不同的是,下标是所管理的连续页的页数。每个下标的链表,管理着相同连续页数的页。数组的最后一个元素,管理着大于255页的连续页的链表。

当有大对象内存分配的时候,先按照4k向上取整,然后根据尺寸,从对应的下标处取链表元素,看是否能取到对应的限制连续页。假设无法取到,则去下一个下标处取(该下标管理的连续页的页数肯定大于之前的下标),假设取到,则进行页分裂,将剩下的页放入对应下标所管理的链表中。

共 0 条回复
暂时没有人回复哦,赶紧抢沙发
发表新回复

作者

sryan
today is a good day