抗碎片化微控制器堆算法

7
我希望在内存受限的微控制器中实现堆分配算法。我已经缩小了搜索范围,只剩下两个选项,但我非常乐意听取有经验的人的建议或意见。 我的要求: -速度当然很重要,但是不是首要关注点。
-确定性时间不重要——任何需要确定性最坏情况时间的代码部分都有自己的分配方法。
-主要要求是免疫碎片化。设备正在运行一个Lua脚本引擎,它将需要一系列分配大小(重点是32字节块)。主要要求是使该设备长时间运行而不会使其堆转变为无法使用的状态。 还请注意: -参考资料,我们谈论的是Cortex-M和PIC32部件,内存范围从128K到16MB(重点关注较低端)。
-我不想使用编译器的堆,因为1)我希望所有编译器具有一致的性能,2)它们的实现通常非常简单,并且对于碎片化来说相同或更差。
-双重间接选项被排除了,因为我不想基本更改和重新验证巨大的Lua代码库。 我迄今为止喜欢的方法: 1)有一个二进制伙伴分配器,并牺牲内存使用效率(舍入到2的幂大小)。 -这将(据我所知)需要为每个顺序/ bin设置一个二叉树,以按内存地址排序的自由节点存储以进行快速伙伴块查找以进行重新链接。
2)为免费块有两个二叉树,一个按大小排序,一个按内存地址排序。(所有二进制树链接都存储在块本身中) -使用表格按大小最佳拟合进行分配,然后从其他树中按地址删除该块 -解除分配会查找相邻块以进行重新链接
-这两种算法都需要在分配块之前存储分配大小,并且块以2的幂减4(或8,具体取决于对齐)出去。 (除非它们在其他地方存储二进制树以按内存地址排序跟踪分配,否则我认为不是一个好的选择)
-这两种算法都需要平衡高度的二叉树代码。
-算法2不需要浪费内存来舍入为2的幂。
-无论哪种情况,我可能会通过嵌套位字段为32字节块分配一个固定的块库,这些块将免疫外部碎片化。 我的问题: -有什么理由认为方法1比方法2更免疫碎片化吗?
-我是否遗漏了任何符合要求的替代方案?

伙伴系统在释放块时不必使用二叉树来查找可能的伙伴。您可以从原始块的地址计算出伙伴的地址。然后,您检查一个位以查看该伙伴是空闲还是已用。您可以将该位放在区域的开头 - 因此您实际上不会获得32个字节,而是可能有31个可用字节。您可以通过使用单独的位图来表示空闲/已用来更改此内容。Knuth描述了一种使用标记位进行空闲/已用的版本。 - mcdowella
是的,我刚想到了。我认为2GB的块大小可能已经足够好了 :) 我认为为了摆脱二叉树中的空闲节点,可以将每个bin/order的空闲节点存储在双向循环列表中,并只使用伙伴的指针来链接下一个和上一个节点(如果它指向要删除的节点,则将头指针向前移动一个节点)。这样就完全不需要搜索以查找和删除空闲伙伴。因此,没有二叉树实现。你有什么想法? - Nathan Wiebe
你真的需要分配内存吗?像这样的嵌入式系统通常不会分配,而是使用固定大小的结构(当然除了栈,你必须小心)。这会给本来应该坚如磐石的东西增加太多风险。 - old_timer
@dwelch 非栈分配在微控制器上非常常见。大多数嵌入式RTOS使用类似堆的分配器,以避免硬编码绝对所有东西的大小。重复分配/释放无法预测大小的块是一个问题。正如我所提到的,我想在系统上运行lua解释器,它主要设计用于较大的平台,并完全依赖于您提供的堆分配器。该系统是高可靠性系统,但lua将用于非关键子系统。 - Nathan Wiebe
2个回答

1

如果块大小没有舍入到2的幂或某个等效值(*), 即使在任何给定时间存在的非永久小对象数量有限,某些分配和释放序列也会产生基本上无限量的碎片化。当然,二进制伙伴分配器将避免这个特定问题。否则,如果使用有限数量的良好相关对象大小但不使用“二进制伙伴”系统,则仍然需要在决定在哪里分配新块时使用一些判断。

另一个要考虑的方法是为预期为永久、临时或半持久的事物采用不同的分配方法。当临时和永久的东西在堆上交错时,碎片化通常会引起最大的麻烦。避免这种交错可能会最小化碎片化。

最后,我知道你真的不想使用双重间接指针,但允许对象重定位可以大大减少与碎片化相关的问题。许多源自微软的微型计算机BASIC使用了垃圾收集字符串堆; 微软的垃圾收集器真的很糟糕,但它的字符串堆方法可以与一个好的垃圾收集器一起使用。


确实非常有帮助! 通过二的幂或等效物,您是在谈论斐波那契伙伴等吗?您能给我一个简单的示例模式,以展示二进制/其他伙伴系统的强度吗?我自己似乎想不出来... - Nathan Wiebe
另外,关于双重间接分配,这将涉及完全重写lua引擎以支持双重间接分配...也许有一个等待被编写的宝石。无论如何,那将是一种可怕的验证量。至于操作系统,它在句柄表中使用了一种形式的双重间接,并且使用相同大小缓冲区的池进行流缓冲,因此我只关心lua。如何模拟/测试/量化内存空间碎片化? - Nathan Wiebe
二的幂伙伴的一个优点是,一些内存用户恰好需要二的幂块大小 - 你提到Lua想要32字节块,一些嵌入式系统可能想要使用二的幂块大小进行FFT。如果您实际上可以使用所有二的幂伙伴块大小(而不必将一位用于空闲/已用),那么这是一个相当好的匹配 - 这就是为什么我编写了一个带有外部空闲/已用位图的伙伴系统演示,只是为了展示它可以完成。 - mcdowella

1
您可以在http://www.mcdowella.demon.co.uk/buddy.html上获取一个(从未用于实际的)Buddy系统分配器,并获得我的祝福,用于任何您喜欢的目的。但我认为您遇到的问题不是仅仅通过插入内存分配器就能轻松解决的。我熟悉的长期高完整性系统具有可预测的资源使用情况,每个资源都有30多页的文档进行描述(主要是CPU和I/O总线带宽-内存很容易因为它们倾向于在每次启动时分配相同的数量,然后再也不分配了)。
在您的情况下,通常的技巧 - 静态分配、空闲列表、堆栈分配 - 都不能起作用,因为-至少按照我们所描述的-您有一个Lua解释器在后台悬停,准备在运行时执行不知道什么操作-如果它只是进入一个循环分配内存直到用尽呢?

您能否将内存使用分为两个部分 - 传统代码在启动时几乎分配了所有需要的内存,之后不再分配;以及可扩展代码(例如Lua),允许在静态分配后从剩余内存中分配所需内存?如果可扩展代码使用了其内存区域的全部空间或者使其碎片化,而不影响传统代码,您是否可以触发重新启动或某种清理操作?


这是一个非常好的观点:设计一个 Lua 引擎定期重启,我开始认为这是不可避免的。而且,Lua 分配完全被隔离在其他内存分配之外。我可以确保所有其他动态内存分配都不会出现碎片化。我只是不能保证即使是已知良好的 Lua 代码和大量的堆空间也不会慢慢地崩溃。我想我可以即时序列化并迁移 Lua 环境的整个内容从一个 Lua 引擎到另一个,但这变得有些疯狂。 - Nathan Wiebe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接