Go-内存调配优化-在结构体中充沛应用内存 (go 内存)
在经常使用Golang启动内存调配时,咱们须要遵照一系列规定。在深化了解这些规定之前,咱们须要先了解变量的对齐形式。
Golang的unsafe包中有一个函数Alignof,签名如下:
funcAlignof(xArbitraryType)uintptr
关于任何类型为v的变量x,AlignOf函数会前往该变量的对齐形式。咱们将对齐形式记为m。如今,Golang确保m是满足变量x的内存地址%m==0的最大或许数,也就是说,变量x的内存地址是m的倍数。
让咱们来看看一些数据类型的对齐形式:
关于结构体中的字段,行为或许会有所不同,具体信息请参考包的文档。
为了更好地理解结构体内存调配的状况,咱们将经常使用unsafe包中的另一个函数Offsetof。该函数前往字段相关于结构体起始位置的位置,换句话说,它前往字段起始位置与结构体起始位置之间的字节数。
funcOffsetof(xArbitraryType)uintptr
为了更好地理解结构体内存调配,让咱们以一个示例结构体为例:
typeExamplestruct{aint8bstringcint8dint32}
如今,咱们将找出类型为Example的变量所占用的总内存,并尝试优化调配。
varv=Example{a:10,b:"Loremipsumdolorsitamet,consecteturadipiscingelit.Vivamusrhoncus.",c:20,d:100,}fmt.Println("字段a的偏移量:",unsafe.Offsetof(v.a))//输入:0fmt.Println("字段b的偏移量:",unsafe.Offsetof(v.b))//输入:8fmt.Println("字段c的偏移量:",unsafe.Offsetof(v.c))//输入:24fmt.Println("字段d的偏移量:",unsafe.Offsetof(v.d))//输入:28
如今,疑问出现了:为什么结构体中字段b的偏移量是8?它应该是1,由于字段a的类型是int8,只占用1个字节。回到字符串数据类型的对齐形式,它的值为8,这象征着地址须要被8整除,因此在其中拔出了7个字节的填充,以确保这种行为。
为什么字段c的偏移量是24?字段b中的字符串看起来比16个字节要长得多,假设字符串的偏移量是8,那么字段c的偏移量应该更大一些。
上述疑问的答案是,在Go中,字符串并不是在结构体内的同一位置调配内存的。有一个独自的数据结构来保管字符串形容符,并且该字符串形容符以原地形式存储在结构体中,用于类型为string的字段,该形容符的大小为16个字节。
如今,让咱们来看看unsafe包中的另一个函数Sizeof。正如其称号所示,该函数预计并前往类型为x的变量所占用的字节数。
留意:它是依据结构体中或许存在的不同大小的字段来预计大小的。
funcSizeof(xArbitraryType)uintptr
如今,让咱们来看看咱们的结构体Example的大小。
fmt.Println("Example的大小:",unsafe.Sizeof(v))//输入:32
咱们如何优化这个结构体以最小化填充呢?
为了优化这个结构体的内存,咱们将检查不同数据类型的对齐形式,并尝试缩小填充。让咱们尝试将两个int8类型的字段放在一同。
typeystruct{aint8cint8bstringdint32}varv=y{}fmt.Println("字段a的偏移量:",unsafe.Offsetof(v.a))//输入:0fmt.Println("字段b的偏移量:",unsafe.Offsetof(v.b))//输入:8fmt.Println("字段c的偏移量:",unsafe.Offsetof(v.c))//输入:1fmt.Println("字段d的偏移量:",unsafe.Offsetof(v.d))//输入:24fmt.Println("Example的大小:",unsafe.Sizeof(v))//输入:32
太棒了,咱们去掉了一些填充,然而为什么大小依然是32?大小应该是1(a)+1(c)+6(填充)+16(b)+4(d)=28
如今,当结构体的最后一个字段与架构的对齐要求不齐全分歧时,会在最后一个字段之后增加填充,以确保结构体的全体大小是其字段中最大对齐要求的倍数。由于字符串数据类型的最大对齐形式为8,所以额外增加了填充,使大小成为8的倍数,即在开端填充了4个字节,使大小为32字节。
咱们能否进一步缩小填充,使其愈加优化?
让咱们尝试经过移动字段位置来成功。
typeystruct{bstringdint32aint8cint8}varv=y{}fmt.Println("字段a的偏移量:",unsafe.Offsetof(v.a))//输入:20fmt.Println("字段b的偏移量:",unsafe.Offsetof(v.b))//输入:0fmt.Println("字段c的偏移量:",unsafe.Offsetof(v.c))//输入:21fmt.Println("字段d的偏移量:",unsafe.Offsetof(v.d))//输入:16fmt.Println("Example的大小:",unsafe.Sizeof(v))//输入:24
咱们可以看到,经过从新陈列字段的位置,使得对齐须要最小化填充,咱们曾经将结构体的大小从32减小到24,这是内存优化的渺小提高,到达了25%。
以后的内存占用是16(b)+4(d)+1(a)+1(b)+2(填充)。
遗憾的是,由于言语和架构的限度,咱们无法进一步去除填充。
(十一)golang 内存分析
编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2。 除了glibc,业界比较出名的内存分配器有Google的tcmalloc和Facebook的jemalloc。 二者在避免内存碎片和性能上均比glic有比较大的优势,在多线程环境中效果更明显。 Golang中也实现了内存分配器,原理与tcmalloc类似,简单的说就是维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存,私有内存不足再从全局申请。 另外,内存分配与GC(废品回收)关系密切,所以了解GC前有必要了解内存分配的原理。 为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存。 以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示: 预申请的内存划分为spans、bitmap、arena三部分。 其中arena即为所谓的堆区,应用中需要的内存从这里分配。 其中spans和bitmap是为了管理arena区而存在的。 arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页; spans区域存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)乘以指针大小8byte = 512M bitmap区域大小也是通过arena计算出来,不过主要用于GC。 span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。 根据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小。 如下表所示: 上表中每列含义如下: class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型 bytes/obj:该class代表对象的字节数 bytes/span:每个span占用堆的字节数,也即页数乘以页大小 objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)上表可见最大的对象是32K大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。 span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小,span将一个或多个页拆分成多个块进行管理。 src/runtime/:mspan定义了其数据结构: 以class 10为例,span和管理的内存如下图所示: spanclass为10,参照class表可得出npages=1,nelems=56,elemsize为144。 其中startAddr是在span初始化时就指定了某个页的地址。 allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配,其allocCount也为2。 next和prev用于将多个span链接起来,这有利于管理多个span,接下来会进行说明。 有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache。 src/runtime/:mcache定义了cache的数据结构 alloc为mspan的指针数组,数组大小为class总数的2倍。 数组中每个元素代表了一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。 根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要GC进行扫描。 mcache和span的对应关系如下图所示: mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况,每种class的span个数也不相同。 上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。 cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central。 src/runtime/:mcentral定义了central数据结构: lock: 线程间互斥锁,防止多线程读写冲突 spanclass : 每个mcentral管理着一组有相同class的span列表 nonempty: 指还有内存可用的span列表 empty: 指没有内存可用的span列表 nmalloc: 指累计分配的对象个数线程从central获取span步骤如下: 将span归还步骤如下: 从mcentral数据结构可见,每个mcentral对象只管理特定的class规格的span。 事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中。 src/runtime/:mheap定义了heap的数据结构: lock: 互斥锁 spans: 指向spans区域,用于映射span和page的关系 bitmap:bitmap的起始地址 arena_start: arena区域首地址 arena_used: 当前arena已使用区域的最大地址 central: 每种class对应的两个mcentral 从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。 mheap内存管理示意图如下: 系统预分配的内存分为spans、bitmap、arean三个区域,通过mheap管理起来。 接下来看内存分配过程。 针对待分配对象的大小不同有不同的分配逻辑: (0, 16B) 且不包含指针的对象: Tiny分配 (0, 16B) 包含指针的对象:正常分配 [16B, 32KB] : 正常分配 (32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴,这里暂时仅关注一般的分配方法。 以申请size为n的内存为例,分配步骤如下: Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。 1、Golang程序启动时申请一大块内存并划分成spans、bitmap、arena区域 2、arena区域按页划分成一个个小块。 3、span管理一个或多个页。 4、mcentral管理多个span供线程申请使用 5、mcache作为线程私有资源,资源来源于mcentral。
iOS - 结构体内存分配
CGPoint在OC中是一个结构体,结构体一般采用内存对齐的方式分配。 1、结构体每个成员相对于结构体首地址的偏移量都是这个成员大小的整数倍,如果有需要,编译器会在成员之间加上填充字节。 2、结构体的总大小为结构体最宽成员大小的整数倍。 3、结构体变量的首地址能够被其最宽基本类型成员的大小所整除。 4、对于结构体成员属性中包含结构体变量的复合型结构体,在确定最宽基本类型成员时,应当包括复合类型成员的子成员。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。 5、总结:结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:sizeof(struct) = offsetof( last item) + sizeof (last item) + sizeof( trailing padding)
例1:
第1个成员相对结构体首地址的偏移量为0,是成员int i(长度为4)的整数倍。 第2个成员相对结构体首地址的偏移量为4,是成员char c (长度为1)的整倍。(因为结构体总大小为结构体最宽成员大小的整数位,所以如果此结构体只有这两个成员的话,会在char c后添加3个填充字节,但现在有3个成员,所以不需要填充。) 第3个成员相对结构体首地址的偏移量为5,不是成员int x 的整数倍,所以在x前(或者说是c之后)填充3个字节,以使x的偏移量达到8而成为4的整数倍。所以这个结构体占内存大小为4+1+3+4。
例2:成员个数与每个成员类型都一个,只不过顺序不一样,占内存大小就不一样。
例3:复合型结构体
#pragma pack(n)//编译器将按钮N个字节对齐,设置结构体最宽成员大小(与实际最宽成员大小取小)。即结构体最终长度是n的整数倍。 #pragma pack() //取消自定义对齐方式。 #pragma pack(puch,1) //把原来对齐方式保存起来,并设置新的对齐方式。 #pragma pack(pop)//恢复之前保存的的对齐状态 Expected #pragma pack parameter to be 1, 2, 4, 8, or 16 预期的#pragma pack参数为1,2,4,8或16
设置对齐方式之后的内存计算 1、当设置的对齐长度小于当前成员长度时,成员偏移量是成员长度的整数倍。 2、当设置的对齐长度大于当前成员长度,并小于最长成员长度时,成员偏移量是设置的对齐长度的整数倍。 3、当设置的对齐长度大于最长成员长度时,成员成员偏移量按当前成员的实际大小对齐。 4、当设置的对齐长度小于实际最长成员长度时,结构体长度为设置的对齐长度的整数倍。 5、当设置的对齐长度大于或等于实际最长成员长度时,结构体长度为实际最长成员长度的整数倍。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。