使用堆内存(malloc/new)会创建一个不确定性程序吗?

79

我几个月前开始使用C语言为空间应用程序和使用C++语言的微控制器开发实时系统软件。在这类系统中,有一个经验法则,就是永远不要创建堆对象(因此没有malloc/new),因为这会使程序不确定性。当别人告诉我这个经验法则时,我无法验证其正确性。那么,这是一个正确的说法吗?

对我来说纠结的地方在于,据我所知,确定性意味着再次运行程序将导致完全相同的执行路径。就我的理解而言,在多线程系统中会出现这个问题,因为每次运行相同的程序可能会按不同的顺序运行不同的线程。


12
动态内存分配和释放存在内存碎片问题,在实时应用程序中这是不可接受的。 - Gaurav Pathak
24
基本上是这样的:PC程序员学习使用malloc/new。然后他们来到嵌入式系统,但由于嵌入式系统具有完全不同的架构和思维方式,堆栈在这里就不再有任何意义了。PC程序员感到沮丧,因为学校没有教他们如何在嵌入式系统中使用堆栈!而且他们已经在PC上使用堆栈很长时间了!PC程序员无视其他人并继续使用堆栈分配内存。结果程序变得糟糕,充满错误和性能问题。PC程序员被解雇。嵌入式系统程序员接手处理混乱。程序被重新从头编写。 - Lundin
11
作为一名获得过粒子物理学和原子物理学博士学位的物理学家,我已经发现很难区分计算机中的确定性和非确定性,因为我们生活在一个非确定性的宇宙中。我知道我的问题很容易引起循环论证,即在某些假设下是否存在确定性,才能将其称为确定性。但这里的确定性语言使用仅限于计算机科学家的用法,与自然界中的真实随机性没有实际关联。 - The Quantum Physicist
9
在现代几乎所有的计算机上做非确定性的事情都是微不足道的。其中一种简单的方法是在等待磁盘读取完成的例程中注意处理器指令计数器的最低有效位。它受到磁盘旋转速度的剪切湍流的影响。你可以通过类似的方式处理网络数据包,这受到石英晶体微观区域温度变化的影响,进而影响网络接口时钟和CPU时钟之间的偏差。你还可以从音频输入中提取热噪声。还有许多其他的方法。 - David Schwartz
6
现在大多数x86 CPU(自IvyBridge时代Intel开始)都内置了一个名为rdrand的指令,您可以从一个普通的用户空间进程中执行它。 它从热噪声发生器提供真正的硬件随机性,并使用AES进行条件处理(除非NSA削弱了设计…)。 当然,正如David指出的那样,rdtsc也是不确定的,特别是考虑到仅一个进程,但他提出了不同时钟域之间的同步给出了一些真正的不确定性。 - Peter Cordes
显示剩余15条评论
12个回答

0
malloc, free, new, delete 有各种技术可以减轻动态内存分配的问题。例如,您可以使用“就地新建”运算符分配所有对象,或者在启动时使用堆一次性分配它们。然而,如果无法避免动态内存分配,可以使用专门的内存分配器实现。
实时堆分配器
不同平台使用了各种堆分配算法,如Dlmalloc、Phkmalloc、ptmalloc、jemalloc、Google Chrome的PartitionAlloc和glibc堆分配器。虽然每种算法都有其优点,但它们并不适用于优先考虑速度、确定性、最小碎片化和内存安全的硬实时环境。
实时堆分配器的主要要求是:
可预测的执行时间:'malloc, free'和'new delete C++'函数的最坏情况执行时间必须是确定性的,并且与应用程序数据无关。
内存池保留:算法必须努力减少耗尽内存池的可能性。这可以通过减少碎片化和最小化内存浪费来实现。
碎片化管理:算法应有效地管理和减少外部碎片化,这可以限制可用的空闲内存量。 定义行为:分配器必须致力于消除任何未定义行为,以确保其操作的一致性和可靠性。 功能安全:分配器必须遵守功能安全原则。在正常和异常条件下,它应始终执行其预期功能。其设计必须考虑并减轻可能的故障模式、错误和故障。
当我们谈论RTSHA中的“功能安全”时,我们并不是指“安全”。 “功能安全”是指系统设计的一个方面,确保它对输入和故障做出正确响应,最大限度地减少物理伤害的风险,而“安全”是指采取措施保护系统免受未经授权的访问、干扰或损坏的影响。 错误检测和处理:分配器应具有检测和处理内存分配错误或失败的机制。这可以包括强大的错误报告,并在分配失败的情况下提供回退或恢复策略。 支持不同算法:分配器应具备足够的灵活性,以支持不同的内存分配算法,使其能够适应不同应用的特定需求。 可配置性:分配器应可配置以适应特定平台和应用的要求。这包括调整内存池大小、分配块大小和分配策略等参数。
效率:分配器应该在时间和空间方面都高效。它应该追求最小的开销和快速的分配和释放时间。
可读性和可维护性:分配器的代码应该清晰、有良好的文档,并且易于维护。这包括遵循良好的编码实践,如使用有意义的变量名和包含解释代码的注释。
兼容性:分配器应该与其设计的系统兼容,并与系统的其他组件良好地配合工作。
我编写的实时安全堆分配器(RTSHA),可以在GitHub上找到,是一个超快的内存管理系统,旨在满足这些要求。
RTSHA支持几种不同的堆分配算法:
小型固定内存页
这个算法通常用于特定情况下频繁分配和释放某个特定大小对象的内存管理。通过使用“固定块大小”算法,大大简化了内存分配过程并减少了碎片化。
内存被分成固定大小的页面块(32、64、128、256和512字节)。当有分配请求时,可以简单地给予其中一个块。这意味着分配器不需要在堆中搜索合适大小的块,从而提高性能。空闲块内存用作“空闲列表”存储。该列表使用标准链表实现。然而,通过启用预编译选项USE_STL_LIST,也可以使用STL版本的前向列表。两种实现之间没有显著的性能差异。
释放操作也很简单,只需将块添加回可用块列表即可。与其他一些分配策略相比,无需合并相邻的空闲块,这也可以提高性能。
然而,固定块大小的分配并不适用于所有场景。它在大多数分配都是相同大小或少量不同大小的情况下效果最好。如果分配请求的大小差异很大,那么这种方法可能会导致大量的内存浪费,因为小的分配占用整个块,而大的分配则需要多个块。
"Small Fix Memory Page"也被"Power Two Memory Page"和"Big Memory Page"算法在内部使用。

双倍内存页面

这个算法是一个更复杂的系统,它只允许大小为2的幂的块。这种设计使得合并自由块变得更容易,并且显著减少了碎片化。该算法的核心基于一组自由列表的数组。每个列表对应一组大小为2的幂的块。例如,有一个专门用于64字节自由块的列表,另一个用于128字节块,依此类推。这种结构化方法确保特定大小的块可以随时使用,优化内存管理和访问。这种方法确保了高效的块分配和释放操作,充分利用了2的幂大小约束。通过将2的幂块大小与自由列表数组和二分搜索机制相结合,该算法在内存效率和操作速度之间取得了平衡。这是一种相当高效的内存分配方法,特别适用于内存碎片化是一个重要问题的系统。该算法将内存划分为分区,以尽量减少碎片化,而“最佳适配”算法则在页面中搜索最小的足够满足分配需求的块。此外,由于其算法化的内存分配和释放方法,该系统对于故障具有抵抗能力。合并操作有助于确保在释放后可以重新形成大的连续内存块,从而减少随时间推移的碎片化可能性。合并依赖于具有相同大小的自由块可用,但并非总是如此,因此该系统并不能完全消除碎片化,而是旨在将其最小化。 Cortex-M7上的性能测试结果。
根据系统分析得出的结果,以下是内存操作的CPU周期性能指标:
小型固定页面:
rtsha_malloc:204个周期 rtsha_free:193个周期
这代表了为处理小于512字节的内存块而进行的内存分配和释放所花费的时间。
Power2页面:
rtsha_malloc:873个周期 rtsha_free:636个周期

1
你在回答这个问题的过程中是否使用了ChatGPT或类似的人工智能技术? - starball
我是Real Time Safety Heap Allocator(RTSHA)的作者。https://github.com/borisRadonic/RTSHA 我偶尔使用ChatGPT来改进和优化我的句子,因为我不是以英语为母语的人。 - Boris Radonic

-3

总是需要进行权衡。程序的运行环境和执行的任务应该是决定是否使用 HEAP 的基础。

当你想在多个函数调用之间共享数据时,堆对象非常高效。只需传递指针,因为堆是全局可访问的。但也存在一些缺点,例如某些函数可能释放了此内存,但仍可能在其他地方存在一些引用。

如果在完成工作后未释放堆内存并且程序继续分配更多内存,则在某些时候 HEAP 将耗尽内存并影响程序的确定性特性。


1
你只需要传递指针,因为堆是全局可访问的。这与.data.bss有什么不同...? - Lundin
5
可以创建全局堆栈变量...这有什么问题吗?它们也可以在函数之间传递。 - The Quantum Physicist
2
这似乎没有回答所提出的问题,而是回答了其他问题。提醒一下,问题是“使用堆会使程序非确定性吗?”这并没有回答这个问题。它可能回答了问题“在实时系统中使用堆是否是一个好主意?”,但那是一个不同的问题。最后一句话开始朝着那个方向发展,但你没有说明它如何影响程序的确定特性,也没有回答这个问题。 - D.W.

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