架构设计内容分享(一百四十七):中间件内存池设计初探

本文探讨了内存池设计在中间件中的应用,涉及内存池地址问题、空闲块划分策略,重点介绍了二级索引表以支持动态内存分配和性能优化。文章强调了内存池设计的灵活性与适用性,指出复杂设计可能影响效率。
摘要由CSDN通过智能技术生成

目录

一、前言

二、中间件内存池设计    

2.1中间件内存池地址问题

2.2中间件内存池空闲块划分

2.3中间件内存池空闲块索引

2.4二级索引表内存池介绍    

三、结束语


一、前言

内存池主要是为了解决“内存复用时,系统产生的额外开销”的问题。内存池可以避免重复创建和销毁,过多的系统调用,进而提高性能。这个技术也称为池化技术,本质上就是为了复用,减少开销,提升性能。

这种开销有如下几类:

1. 效率开销(性能开销),频繁的系统调用等等操作,对性能影响很大。

2. 减少内存碎片(内存浪费),碎片化指的是内存被分割的不连续,导致后续的内存分配无法实现,但是实际上系统中仍然有很多内存未被使用。

内存池如名字所言,在设计时需要先把池子中注入一些内存,要用内存的时候,直接去池子中取,不需要再从系统中分配了。这是池化技术的缺点,必须要预先分配一段空间,预分配空间过小,则不够使用,过大则浪费较多。

对于内存池设计,目前主流的内存池设计均是围绕着heap(堆)。著名的tcmalloc开源库就是这类典型的代表。当然开源界还有很多相关的代表。

但是,中间件的内存池需要围绕着Share Memory进行设计,典型的代表是Iceoryx。但是Iceoryx基于静态配置设计,一旦配置完成,无法动态扩展。因此,就中间件的内存池设计而言,并没有一个基于Share Memory并且支持动态扩展的内存池设计。

我们进一步推广一下,基于无属性Share Memory的内存池设计,无属性Share Memory表示其不关心内存是MCU的.data段的大数组分配,还是Linux侧共享内存分配,亦或是Linux侧的堆分配,亦或者Linux侧的大数组全局变量分配。我们尝试将多平台的内存池整合成到一个架构中,当然代码肯定会有所区别,但是原理一致。

最后,必须要强调一点,这种设计并无好坏之分,只有适合不适合的区别。本文仅做一些探讨。

二、中间件内存池设计    

2.1中间件内存池地址问题

内存池作为一段整体,实际上需要其他的数据结构去辅助内存池设计。在MCU上,辅助代码可以放在Flash上,内存放在RAM中,所有进程通过同一个地址可以访问到同样的物理地址。在Linux上,如果存放在同一个进程空间,那么内存池地址可以复用。如果是不同的进程空间访问同一块Share Memory,那么就需要做地址的二次映射。基于Share Memory的地址映射方案可参考Iceoryx的参考地址设计。

2.2中间件内存池空闲块划分

为了能够分配不同大小的内存块,必然要缓存不同大小的内存块。那么,不同大小需要准备多少个不同的大小?8字节的内存块、16字节、64字节、128字节…..1024字节等等的内存块?

以Iceoryx的内存池为例,根据配置划分不同大小的内存块,如果不主动进行配置,则使用默认配置。第一次分配时,直接划分64Kbyte,只浪费了100字节,但是等多次分配之后,池子中只有大块的内存,当申请小字节的内存时,只能将大块内存分配给用户,导致浪费严重。

为了解决资源浪费的问题,很多基于heap的内存池会将多余的内存划分出去,便于重新利用。如下图所示,每一次分配都会将多余的字节划分到其他段中,但是还是有少量的内存浪费,这种浪费已经在可接受的范围内了。

2.3中间件内存池空闲块索引

上述这些内存池的空闲块划分都是按照固定大小划分的。当用户申请某某字节长度大小的内存块时,会把这个长度映射转成内存块的地址。比如说申请16字节大小内存,会找到16字节大小空闲块的内存地址,将其从内存池中移除,返回地址给用户,整体的流程如下图所示。   

图片

如何去寻找最适合的内存块是值得去思考的。Iceoryx通过数组循环的方式去索引合适的空闲块。经典的内存池设计都是通过“桶-自由链表”的方式实现空闲块的索引。以这张图为例,任何字节会被索引成8Byte,16Byte,….,64KByte,256KByte的位置,这个位置就是一个桶,桶中装有链表,这个链表是自由链表,也就是头插式的链表,性能较高。

2.4二级索引表内存池介绍    

如上这些方式在内存池的设计中很常见。这边再介绍一种通过2级索引表索引内存块的方法。假设我们想要管理1G大小的内存池,也就是2^30字节。2G内存占据的地址空间为0x0000 0000~0x3FFF FFFF,总计30个bit,最高位为bit29。假设一级索引偏移6个bit,则一级索引需要一个30-6=24位的数据即可,最高位为bit23。对一级索引得到的二级索引再次拆分32份即(bit0~bit31),32份正好可以用一个uint32的变量表示。那么最终的一级和二级索引如下所示:

uint32_t  FirstLevel_Index;  //有效位24bit

uint32_t  SecondLevel_Index[24]; //有效位32bit

addr*   ToTalTable_Index[24][32];   //最终的索引表

我们假设申请数据块大小为a,在数学模型中a为给定数值,一级索引的值为未知解x1,二级索引的值为未知解x2,此外,x1最多为24bit且x1左移6bit可以还原30bit数据,二级索引拆分为32份,32换算为2^5,可以得出一个参数5,那么申请数据块大小与二级索引表的数学模型如下:

图片

图片

我们需要推导得到给定数值a和未知解x1之间的映射函数f1,以及给定数值a和未知解x1、x2之间的映射函数f2。msb(a)特指获取a的最高有效位

根据题意, x1会偏移6位,那么可得如下函数:

图片

根据题意,x2需要分成32份,那么尝试得到如下函数:

图片

将函数整理一下,得到一些有趣的表达式,如下所示:

图片

图片

    

最终整理可得:

图片

当x1固定时,a的取值范围的差值rangex1为:

图片

当x1固定并且x2也固定时,a的取值范围差值rangex2为:

图片

上述表达式也就是意味着,二级索引的每一条子索引(单条范围)都由一级索引决定,二级索引平等的划分一级索引空间范围。

x1/bit位置

rangex1/字节

取值范围/字节

单条二级索引大小/字节

0

64

64 ~ 1270 ~ 127

4

1

128

128 ~ 255

4

2

256

256 ~ 511

8

3

512

512 ~ 1023

16

4

1024

1024 ~ 2047

32

5

2048

2048 ~ 4095

64

6

4096

4096 ~ 8191

128

7

8192

8192 ~ 16383

256

8

16384

16384 ~ 32K - 1

512

9

32K

32K ~ 64K - 1

1K

..

22

256M

256M ~ 512M - 1

8M

23

512M

512M ~ 1024M - 1

16M

为了不浪费空间,对x1等于0时进行特殊处理,让其管控0 ~ 127字节的范围。

综上所述,通过二级索引表可以索引到任意大小范围的空闲块,并且小空间的单条范围也相应的减少,大空间的数据块单条范围相应的变大,这种划分比较符合实际需求。

我们举例说明如何进行索引,假设我们需求1022字节的空间,计算式如下:

图片

图片

    

对应的索引表为一级索引为bit3,二级索引为bit31, FirstLevel_Index变量bit3置位,SecondLevel_Index[3]变量bit31置位,ToTalTable_Index[3][31]为想要的地址。整体的流程如下:

 

在空闲块的分配过程中,如果最后只有大数据块,那么大数据块会将不需要的内存再次存储进整个二级索引中。如果一个同一个数据块存在多样,通过“桶-自由链表”的方式可以解决多个相同大小空闲块的索引问题。

三、结束语

内存池分配方案本无好坏之分,有时候最好的方式就是给每一个主题预分配好固定大小的内存块,再对这个内存块按照主题的数据结构大小*数量的方式划分好,按照index逐个索引就行。

上文讲到的多个方案,需要根据实际的情形去使用,过于复杂的内存池设计方案实际会极大的影响程序的运行效率。

本文仅仅是初探内存池设计,关于内存池的互斥访问,内存碎片的归类整理等细节,仍待更多讨论。    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值