一站式电子元器件采购平台

华强商城公众号

一站式电子元器件采购平台

元器件移动商城,随时随地采购

华强商城M站

元器件移动商城,随时随地采购

半导体行业观察第一站!

芯八哥公众号

半导体行业观察第一站!

专注电子产业链,坚持深度原创

华强微电子公众号

专注电子产业链,
坚持深度原创

电子元器件原材料采购信息平台

华强电子网公众号

电子元器件原材料采购
信息平台

动态内存分配优化集成Blackfin 处理器软件

来源:analog 发布时间:2024-01-02

摘要: 看看Blackfin处理器架构,它结合了最好的快速片上存储器和分层存储器两种方法。

典型的dsp通常有少量的快速片上存储器。微控制器通常可以访问更大的外部存储器。Blackfin处理器的分层内存架构结合了这两种方法的优点,提供了不同性能水平的多级内存。对于需要最确定性的应用程序,它可以在单核时钟周期内访问片上SRAM。对于具有更大代码大小的系统,可以使用更大的片内和片外内存,但延迟会增加。

就其本身而言,这种层次结构只有适度的用处;今天的高速处理器通常会以更慢的速度运行,因为更大的应用程序只适合较慢的外部存储器。为了提高性能,程序员可以选择手动将关键代码移进或移出内部SRAM。此外,在体系结构中添加数据和指令缓存使得外部内存更易于管理。缓存减少了指令和数据进入处理器核心的手动移动。这极大地简化了编程模型,因为无需担心管理进入内核的数据流和指令。

虽然Blackfin的内存是通用的,易于在许多应用中使用,但也有一些应用,如嵌入式手机系统,其内存分配对任何嵌入式处理器来说都是困难的。在这种应用程序中,指令缓存不提供与手动进出SRAM的数据移动相同级别的代码管理。本文建议使用一种动态内存分配工具来应对这一挑战。

在手机平台协议栈和应用软件的开发中,有效地处理系统中的内存资源是一个关键问题。过去,内存资源是“手工”分配给系统内的每段代码的;但是,视频和语音识别等模块数量的不断增加,使得使用这种方法的解决方案更加难以优化。动态内存分配器可用于在大型应用程序中分配和释放内存,从而无需手动管理此任务。本文描述了动态内存分配的一些原则,并演示了一个具体的实现,该实现考虑了整体系统的考虑,并将Blackfin的内存划分为具有不同属性(价格、速度、双重访问可能性)的不同空间。

内存管理解决方案

在大型嵌入式应用程序中,可以实现几种内存管理方法。主要的方法如下所述。

堆栈。所有变量和缓冲区都可以简单地在函数的顶部声明。它们存储在堆栈空间中,只有在退出函数时才释放该空间。这种解决方案的主要缺点是堆栈增长,例如,堆栈在函数的生命周期内不断增长。它的生存期有时可能很长,因为函数可能是递归的和/或可中断的。

手动重叠。另一种流行的解决方案是使用在链接阶段定义的节对缓冲区的地址进行硬编码。这比在堆栈中分配要灵活一些,因为它允许内存重叠。如果两个模块永远不会互相中断,它们的临时内存可以共享相同的内存段。然而,随着模块数量的增长,这种解决方案确实变得难以管理集成系统。此外,其他内存问题(例如不适当的重叠或给定部分的缓冲区大小不足)可能很难跟踪。更糟糕的是,当一个新特性需要两个以前从未重叠的函数来并发运行时,这就更加困难了。图1显示了一个基于手动重叠实现的示例。


图1所示 手动重叠内存。

动态分配。动态分配支持内存重叠:一旦不需要内存空间,它就被释放并可以重用。与堆栈分配方法不同,动态分配不会导致不受控制的内存空间的增加。实际上,函数使用的内存会在不需要时立即释放,而不是等待函数结束。

在开发动态内存分配器时要考虑哪些特性?

动态内存分配器由两个功能组成:一个是分配内存空间;另一个释放内存。分配保留了一些空间来处理内存请求。当调用free函数时,保留的空间将被释放,并可用于满足进一步的请求。例如,让我们构建一个非常基本的动态内存分配器来理解这样一段代码必须处理的所有权衡。我们将从一些基本定义开始,然后描述分配器。

块。让我们假设分配器可以为所需的内存提供一个大内存空间的块。很容易理解,整个空间不能被带走,以满足第一个要求。相反,初始内存空间可以分成不同大小的不同块。

头。当提出内存请求时,我们如何知道给定的部分足够大?这个大小必须保存在内存的某个地方。其中一种解决方案是将其保存在块内的标头中。这是内存开销的一部分。此外,头中至少需要一个比特来表示块是空闲的还是正在使用。

在大块中徘徊。如果第一个数据块太小,我们如何跳到下一个数据块?如果所有块在内存中都是连续的,那么知道块的大小就足以跳转到下一个块。另一种解决方案是在头文件中保留一个指向下一个数据块的指针——这就是链表的原理。

找到合适的。我们如何选择哪个空闲块将为请求服务?一个必要条件是找到一个空闲块,其大小至少是所需的大小。然后可以使用满足此需求的第一个块。这种策略被称为第一匹配。另一个策略,即最佳匹配策略,包括寻找能够容纳请求的最小空闲块。这是动态内存分配器必须处理的最具挑战性的难题:速度与内存大小。第一次匹配很快,但可能会导致巨大的记忆损失,而寻找最佳匹配的替代方法需要时间。可以通过使用几个块链表(bin)来达成妥协,其中每个列表都有类似大小的块。最适合策略选择bin,而第一适合策略选择bin中的块。

碎片。另一种解决方案包括使用first-fit策略,并释放比请求大的块的末尾。这种解决方案的一个缺点是,内存很快就会由几个分散的未使用内存块(大小不同,通常很小)组成。由于产生的空闲空间很小,因此未来的分配很困难。这种情况称为内存碎片。

为了加快请求的速度,一些分配器基于空闲块的链表。这节省了一些时间,因为搜索可以避免考虑所有正在使用的块。然而,这种方法也有缺点。如果只将空闲块保存在列表中,则很难将所有空闲块连续放置在内存中;这个问题使分配器无法获取两个相邻的中等块并将它们放在一起(或合并它们)以构建一个更大的块。


图2 动态分配器的例子。

我们现在已经介绍了所有的概念和妥协,以理解为Blackfin移动电话系统设计的分配器:alloc。

当前实现:alloc

不断增加的信号处理特性(例如新的视频和音频标准)促使了分配器的开发,该分配器被称为用于手机应用程序的alloc。它旨在帮助缩短使用该处理器的产品的上市时间(通过避免不必要的内存重叠)和成本(通过减少峰值内存使用)。

基本原则

当前的实现更关注速度性能,而不是内存开销。内存被划分为多个bin。每个bin存储大小相等的内存块。存储箱中的块具有连续的地址,允许从一个块快速跳转到下一个块。查找适合请求的块的策略是最适合bin并首先适合bin—即第一个空闲块,因为所有块都具有相同的大小。此外,选择箱子中块的大小是为了方便找到最佳箱子:它们都以2的幂相关。bin (N+1)中的块是bin N中的块的两倍大小(bin N也可能包含0个块…)


图3 alloc的Bins/Chunks配置。

一些软件模块可能偶尔需要一个“大”块。然而,如果允许使用大的块,内存将被划分为非常少的块。与其使用一个大的块,不如使用两个较小的块,在少数需要的情况下将它们合并在一起形成一个大块。因此,允许将两个块合并在一起。

为了保证速度,每个块都有一个标头,表明它是否可用并已合并。在合并块的情况下,合并伙伴或“伙伴”的大小保留在标题中。这用于在夫妇被释放时快速恢复伙伴的头部。


图4 块在分配。

Blackfin有什么特殊之处

Blackfin为内存分配器增加了另一个维度:它的数据内存空间被划分为几个内存级别。存储器级别在价格、速度和双重访问可能性方面具有不同的特点:

  • 外部内存Lext很大,使用成本更低,但访问时的延迟更高。

  • 片上存储器L1具有快速访问。它本身被分成不同的银行和子银行,允许同时从不同的子银行访问两项数据(双重访问)。

  • L2在价格和速度方面介于两者之间。但是,可以通过将其缓存到L1来提高其速度。缓存是一个额外的维度。

堆栈。尽管(如前所述)在Stack中分配所有变量不是一个好的解决方案,但仍然需要Stack。对于较小的缓冲区、循环计数器和索引,没有必要因为分配而丢失周期。然而,在系统集成阶段之前,某些缓冲区的分配(堆栈或动态)可能存在一些不确定性。这就是为什么Stack被看作是一个额外的内存级别。

缓存。如上所述,Blackfin可以将L2内存缓存到L1或L1的部分。在这种情况下,不必为新内存重新调整分配器的代码是有利的。在初始化期间,分配器能够从一些专用的Blackfin寄存器中读取缓存配置,然后决定它的bin和chunk。然而,由于分配器必须在任何平台上进行测试,它必须保持最低限度的blackfin特异性。只有数据缓存配置是特定于blackfin的。除此之外,该分配器可以在PC上使用Blackfin以外的编译器进行全面测试。唯一的区别是内存资源的选择与平台的速度或双访问特性无关。

具有上述特点的alloc成为一个重要的软件。因此,它应该尽可能“灵活”,只要这不会过度影响循环次数。

分配器的灵活性

宏。c -宏在alloc实现中被广泛使用。事实上,alloc本身就是一个宏。第一个好处是能够快速地用一个分配器替换另一个分配器,而不必重写调用alloc的所有软件。例如,这可以用于调查不同动态分配器的性能。

Alloca。宏的另一个优点是能够使用Stack作为内存级别,而不必以比使用malloc更复杂的方式调用分配器。实际上,不能通过函数调用来实现Stack中的分配。相反,当以Stack作为内存级别调用alloc时,会执行' alloca '。(大多数编译器都可以使用Alloca。它只在执行alloca指令时才在Stack上保留空间——不像在函数顶部的Stack上声明为函数的生命周期保留空间。)宏alloc测试所需的内存级别,并将其重定向到一个alloca或对分配器_alloc的函数调用。


图5 通过alloc分配堆栈。

所需内存级别的存储。能够在Blackfin上处理不同的记忆级别是一个很大的优势。为了充分利用这个特性,内存级别在编译时不是固定的。因此,对于每个分配,分配器允许测试不同的内存级别,而不必重写或重新编译软件模块的C代码。软件模块附带一个表,该表包含这样或那样的分配所需的内存级别。表的内容可以在运行时更改,只需在特定地址写入新的所需内存级别即可。然而,应该注意的是,如果不能提供所需的内存级别,分配器会选择另一个级别——在内存访问速度方面最接近的级别。


图6 输入表:所需的内存级别。

更改Bins/Chunks配置。alloc的另一个灵活特性是无需重新编译分配器代码即可更改bin和chunk配置。实际上,定义此配置的所有变量都保存到表中。在初始化期间读取表。在任何时候都可以更改表的内容——这将在下次调用初始化时修改bin /chunks配置。不需要在编译时修复bin /chunks分割,作为下一个特性,在分配器周围有一个智能包装器,可以动态调整内存大小。我们也可以想象一个系统运行两个连续的任务,需要两种不同的内存配置。当一个任务完成时,使用最适合第二个任务的配置调用分配器初始化。

最后,alloc有两种派生形式:第一种用于开发和集成,第二种用于最终产品。在开发过程中,调试特性是必需的。下一节将进一步详细介绍当前的实现,以及如何充分利用调试特性。

调试特性如何改进实现

使用内存分配器时的常见问题是由于分配器导致的效率低下,以及不能正确分配和释放内存的风险,这主要导致内存泄漏。

分配器知道内存分区。它还知道所请求的内存量以及哪些内存地址是空闲的。这允许开发调试功能,以采取措施避免内存泄漏。

追踪一个自由的那个已经被遗忘的人。内存泄漏的第一个原因发生在分配了内存但从未释放内存时。这很容易预防。在调试模式下(不是在正常模式下,因为这个测试需要很多周期),分配器构建内存使用的统计信息。如果最后的报告显示一些内存空间仍在使用中,这意味着已经忘记了一个空闲空间。为了更深入地跟踪问题,可以使用另一个报告,其中包含缓冲区名称、它们的地址,以及它们是否被释放或分配(每次调用分配器或free函数时构建报告)。


图7 如何去追寻一份已经被遗忘的自由。

跟踪是否使用了超过预留的空间。另一种类型的泄漏发生在缓冲区分配的空间少于它所需要的空间,并且开始使用已分配给它的空间之外的空间时。在调试模式下,分配器用一个特殊代码“标记”所有空闲的内存空间(这个代码作为“真实”基准的概率非常低)。它不仅标记空闲块,而且还包括分配不需要的块内的所有地址。在每个分配的块中,所需的大小也作为分配块的一部分保留。因此,每次进入分配器(用于新分配或空闲分配)时,它都会验证:

  • 空闲块只包含特殊的代码

  • 分配的块包含所需大小和块末尾之间的特殊代码

执行此检查的函数也可以在分配器之外的任何时间调用。当注意到泄漏时,将构建一条消息并传递给另一个模块,该模块以一种或另一种形式输出(屏幕,特殊可视化工具,用于实时分析的高速记录器等)。


图8。跟踪分配器消息(泄漏情况)的查看器示例。

帮助选择bin /chunk配置。分配器调试功能还可以部分解决有关分配器效率低下的问题。在调试模式下,分配器保存所需内存与已分配内存、每个bin使用的块数量等数据。这提供了一种简单的方法来避免严重的效率低下,例如使用一些从未使用过的容器大小。


图9 捕获数据以帮助选择最佳的bin /Chunks配置。

内存级别之间的内存重分区。一个大问题是如何在不同的软件之间分配内存级别。显然,快速访问内存最适合每一段代码。然而,由于记忆有限,必须做出选择。只有在将整个软件模块构建到系统中时才能做出这种选择。通常时间紧迫的任务需要最快的内存。分配器可以帮助做出这样的选择。

分配器非常有用,因为它可以与包装器一起交付,包装器负责为特定模块运行所有可能的内存配置,同时节省所需的周期数。这有助于了解无法为特定缓冲区获得最快内存对周期的影响。

表1性能矩阵

表中索引L1_B通过/失败L2通过/失败Lext通过/失败
pChannelInstance-82年通过
-71年通过
-119年通过
pSharedMemStruct
-73年通过
-66年
通过
-109年通过
pShared_BurstDec_CCDec_Interleave
94通过
56通过
-48年通过
pShared_EQ_CCDec_Mod_Info
5通过
-81年
通过
-67年通过
CC_Dec_IO_EDGE_PDTCH
130 *通过
-74年
通过
-324年通过
pDeInterleave-232年通过
-57年
通过
18115通过
pOutHeader15通过
-116年
通过
506通过
pScratch_Header_Decoder-281年通过
- 83
通过
3719通过
度规-82年通过
10440
通过
123346通过
pPathMetric-417年通过
-84年
通过
77394通过
pOutRLC_Data-199年通过
-83年通过
1832通过
pScratch_Data_Decoder-75年通过
450通过
23624通过

表中显示的数字表示与参考配置相比,在新配置中运行单元测试所需的周期数的差异。参考配置是由模块的编写器默认提供的。PASS表示在新配置上运行单元测试的结果与运行参考配置的结果相同。周期参考数为:128078。


图10 单元测试流程图。

包装器运行软件模块单元测试(UT)。当分配器第一次运行指针时,它被要求返回指针的名称和查找内存级别的表的地址。在收集了需要查找内存级别的所有地址之后,包装器为所有可能的内存配置重新运行UT。

结论

当前的alloc实现是动态内存分配器的一种可能实现。它的使用表明,当前实现中最有用的特性是调试特性。它们减少了与动态分配相关的风险(尤其是泄漏风险)。同时,它们有助于更好地管理复杂的记忆结构。现在,在手机应用程序中,在Blackfin中添加新的软件模块变得更加容易,而不必重新划分模块之间的内存。

致谢

作者感谢Blackfin应用小组的Rick Gentile和DSP/系统小组组长Zoran Zvonar的宝贵贡献。

声明:本文观点仅代表作者本人,不代表华强商城的观点和立场。如有侵权或者其他问题,请联系本站修改或删除。

社群二维码

关注“华强商城“微信公众号

调查问卷

请问您是:

您希望看到什么内容: