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

Posted

技术标签:

【中文标题】如何通过良好的设计保留堆栈空间?【英文标题】:How to preserve stack space with good design? 【发布时间】:2010-09-13 05:04:04 【问题描述】:

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

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

是否有其他方法可以使代码保持良好的组织性和可读性,同时保留内存?

【问题讨论】:

【参考方案1】:

我想你可能在想象一个这里不存在的问题。大多数编译器在堆栈上“分配”自动变量时实际上并没有做任何事情。

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

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

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

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

【讨论】:

【参考方案2】:

是的,RTOS 确实会占用 RAM 以供任务堆栈使用。我的经验是,作为 RTOS 的新用户,往往会使用不必要的任务。

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

【参考方案4】:

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

正常运行后,读取堆栈空间,看看有多少堆栈空间在运行过程中没有被替换。设计为至少保留 150% 的空间,以解决所有可能未执行的模糊代码路径。

【讨论】:

不,不是。我的观点是您可能无法实现 100% 的代码覆盖率,并且可能缺少一些代码路径。只是我遵循的经验法则。【参考方案5】:

您的堆栈使用有 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 支持线程本地存储,它在每个线程的基础上分配“全局”存储。这可能允许您在每个任务的基础上拥有多个独立的全局变量,但这会使您的代码不那么简单。

【讨论】:

【参考方案6】:

根据您的编译器以及您的优化选项的积极程度,您进行的每个函数调用都会使用堆栈。因此,首先您可能需要限制函数调用的深度。 一些编译器确实对简单的函数使用跳转而不是分支,这将减少堆栈的使用。显然,您可以通过使用汇编宏来跳转到您的函数而不是直接调用函数来做同样的事情。

正如在其他答案中提到的,内联是一种可用的选项,尽管这确实以更大的代码大小为代价。

另一个吃栈的地方是本地参数。您确实可以控制这个区域。使用(文件级)静态将避免以静态内存分配为代价的堆栈分配。全局变量也是如此。

在(真正的)极端情况下,您可以为函数制定一个约定,使用固定数量的全局变量作为临时存储来代替堆栈上的局部变量。棘手的一点是确保不会同时调用任何使用相同全局变量的函数。 (因此约定)

【讨论】:

【参考方案7】:

你能用全局变量替换一些局部变量吗? 数组尤其会吃掉堆栈。

如果情况允许您在函数之间共享一些全局变量, 你有机会减少你的记忆足迹。

权衡成本是增加复杂性,以及功能之间不必要的副作用的更大风险与可能更小的内存占用空间。

您的函数中有哪些类型的变量? 我们在谈论什么大小和限制?

【讨论】:

【参考方案8】:

如果您可以腾出大量主内存但只有少量堆栈,我建议评估静态分配。

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

将声明限定为“静态”将它们存储在主内存中而不是堆栈中。它们基本上表现得像全局变量,但仍然允许您避免过度使用全局变量带来的坏习惯。您可以将大型、长期存在的缓冲区/变量声明为静态以减少堆栈压力。

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

【讨论】:

用于堆栈的 RAM 和用于静态分配的 RAM 通常没有质的区别。您应该通过诸如链接器控制文件之类的东西来控制分配。除非您有一个具有多个 RAM 组的复杂处理器,例如片上 RAM 和单独的外部 RAM。【参考方案9】:

尝试使调用堆栈更平坦,所以不要调用a() 调用b() 调用c() 调用d(),而是让a() 调用b()c()d() 本身.

如果一个函数只被引用一次,请将其标记为inline(假设您的编译器支持此功能)。

【讨论】:

【参考方案10】:

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

使用 GCC,尝试添加“-finline-functions”(或 -O3)标志,也可能添加“-finline-limit=n”标志。

【讨论】:

以上是关于如何通过良好的设计保留堆栈空间?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的编译器保留的空间比函数堆栈帧所需的空间多?

为啥编译器保留一点堆栈空间而不是整个数组大小?

为啥编译器保留一点堆栈空间而不是整个数组大小?

取消隐藏后如何保留堆栈视图子项的约束?

线程的堆栈

转---队列堆栈堆栈的区别