如何通过良好的设计保留堆栈空间?

时间:2020-03-06 14:44:30  来源:igfitidea点击:

我正在用C语言编写带有RTOS的RAM受限的嵌入式微控制器。

我经常将代码分解为简短的函数,但是每个函数调用都需要更多的堆栈内存。
每个任务都需要他的堆栈,这是项目中重要的内存使用者之一。

是否有其他方法可以使代码井井有条,可读性强,又可以保留内存?

解决方案

打开优化,特别是积极的内联。编译器应该能够内联方法以最小化调用。根据我们使用的编译器和优化开关,将某些方法标记为"内联"可能会有所帮助(或者可能会被忽略)。

对于GCC,请尝试添加" -finline-functions"(或者-O3)标志,并可能添加" -finline-limit = n"标志。

尝试使调用堆栈更平坦,因此,与其使用a()调用b()来代替a()调用b()来调用c()来调用d(), c()和d()本身。

如果一个函数仅被引用一次,请将其标记为"内联"(假设编译器支持此功能)。

如果我们可以保留大量的主内存,但只有少量的堆栈,我建议评估静态分配。

在C语言中,在函数内部声明的所有变量都是"自动管理的",这意味着它们被分配在堆栈上。

将声明限定为"静态"会将声明存储在主存储器中,而不是存储在堆栈中。它们基本上就像全局变量一样工作,但仍然可以避免过度使用全局变量带来的不良习惯。我们可以将大型的长期存在的缓冲区/变量声明为静态来降低堆栈压力,这是一个很好的例子。

请注意,如果应用程序是多线程的,或者我们使用递归,那么这将无法正常工作。

我们可以用全局变量替换某些局部变量吗?
数组尤其会占用堆栈。

如果情况允许我们在函数之间的某些全局变量之间共享某些全局变量,
我们有机会减少内存占用。

折衷的代价是增加了复杂性,并且与可能较小的存储空间相比,增加了函数之间不希望出现的副作用的风险。

函数中有什么样的变量?
我们在谈论什么大小和限制?

根据编译器以及优化选项的积极程度,我们将对进行的每个函数调用有堆栈使用情况。因此,从一开始,我们可能需要限制函数调用的深度。
一些编译器确实对简单功能使用跳转而不是分支,这会减少堆栈使用量。显然,我们可以通过使用汇编宏(而不是直接调用函数)跳转到函数来完成相同的操作。

如其他答案中所述,内联是可用的一个选项,尽管这样做确实是以更大的代码大小为代价的。

占用堆栈的另一个区域是本地参数。我们确实可以控制此区域。使用(文件级)静态变量将避免堆栈分配,但要以静态ram分配为代价。全球人也是如此。

在(确实)极端情况下,我们可以为使用约定的函数约定,这些约定使用固定数量的全局变量作为临时存储空间来代替堆栈上的局部变量。棘手的一点是要确保使用同一全局变量的所有函数都不会在同一时间被调用。 (因此成为惯例)

堆栈使用情况包含3个组成部分:

  • 函数调用返回地址
  • 函数调用参数
  • 自动(局部)变量

最小化堆栈使用量的关键是最小化参数传递和自动变量。实际函数调用本身的空间消耗非常小。

参数

解决参数问题的一种方法是传递结构(通过指针),而不是传递大量参数。

foo(int a, int b, int c, int d)
{
...
   bar(int a, int b);
}

改为这样做:

struct my_params {
   int a;
   int b;
   int c;
   int d;
};
foo(struct my_params* p)
{
   ...
   bar(p);
};

如果我们传递了许多参数,则此策略很好。如果参数都不同,那么它可能对我们而言效果不佳。最后,我们将传递一个包含许多不同参数的大型结构。

自动变量(本地变量)

这往往是堆栈空间的最大消耗者。

  • 数组是杀手.。不要在本地函数中定义数组!
  • 最小化局部变量的数量。
  • 使用必要的最小类型。
  • 如果重新输入不是问题,则可以使用模块静态变量。

请记住,如果只是将所有局部变量从局部作用域移动到模块作用域,则没有节省任何空间。我们用堆栈空间交换了数据段空间。

一些RTOS支持线程本地存储,该线程在每个线程的基础上分配"全局"存储。这可能允许我们在每个任务的基础上拥有多个独立的全局变量,但这将使代码不那么简单。

为了评估嵌入式设置中代码的堆栈要求,我在某处读到了一个技巧,即在开始时用已知的模式(我最喜欢的DEAD以十六进制表示)填充堆栈空间,然后让系统运行一段时间。

正常运行后,请阅读堆栈空间,并查看在操作过程中未更换多少堆栈空间。设计时至少要保留其中的150%,以解决所有可能尚未使用的确定性代码路径。

如果需要开始保留堆栈空间,则应该获得更好的编译器或者更多的内存。

软件通常会增长(新功能,...),因此,如果我们必须通过考虑如何保留堆栈空间来启动项目,那么从一开始就注定要失败。

是的,RTOS确实会消耗掉RAM用于任务堆栈的使用。我的经验是,作为RTOS的新用户,有使用不必要的更多任务的趋势。

对于使用RTOS的嵌入式系统,RAM可能是一种宝贵的商品。为了保留RAM,对于简单功能,通过协作多任务设计以循环方式运行在一个任务中实现多个功能仍然是有效的。从而减少任务总数。

我想我们可能正在想象一个这里不存在的问题。大多数编译器在堆栈上"分配"自动变量时实际上不执行任何操作。

在执行" main()"之前分配堆栈。当我们从函数a()调用函数b()时,紧随a使用的最后一个变量之后的存储区地址将传递给b()。如果b()然后调用函数c(),则这成为b()堆栈的开始,然后c的堆栈在由b()定义的最后一个自动变量之后开始。

请注意,堆栈存储器已经存在并已分配,没有初始化发生,唯一涉及的处理是传递堆栈指针。

唯一的问题就是这三个函数都使用大量存储,然后堆栈必须占用这三个函数的内存。尝试将分配大量存储空间的函数保留在调用堆栈的底部,即不要从它们调用另一个函数。

内存受限系统的另一个技巧是将函数的内存占用部分拆分为单独的自包含函数。