我可以递归多少?我可以递归多少?多少ca!@#QFSD @ $ RFW


19

Arduino Uno板的RAM有限,这意味着可用的调用堆栈有限。有时,递归是实现特定算法的唯一快速选择。因此,考虑到调用堆栈受到严格限制,如何找出在板上运行某个程序的情况下,在堆栈溢出(发生不良情况)之前,您可以负担多少个递归调用?


2
您能否举一个算法的例子,其中递归和非递归解决方案之间的选择如此明确?
Cyber​​gibbons

1
@Cyber​​gibbons最近,我不得不在很短的时间内编写和测试RDP,因此我着手进行递归实现。我并不是说不可能进行迭代,而只是更容易进行递归版本。
asheeshr 2014年

在问题标题中这意味着什么How much ca!@#QFSD@$RFW?我很好奇为什么没有人将其编辑成更有意义的东西(在过去4年中)。
尼克·加蒙

答案是211时间(取决于许多因素):)。在这里查看我的答案:arduino.stackexchange.com/a/51098/7727。@NickGammon,我认为他假装是“诅咒”。这是“递归”一词的玩法。花了我一点时间来解决这个问题。起初非常令人困惑。
加布里埃尔·斯台普斯

哦,我懂了。我以为SE服务器可能出现了小故障:)
尼克·加蒙

Answers:


15

如果您真的想要递归(并且@jippie表示这是个坏主意;潜意识中的消息: 不要这样做)并且想知道可以递归多少,那么您将不得不进行一些计算和实验;同样,由于递归函数被调用时,它很大程度上取决于内存状态,因此通常只具有一个近似值。

为此,您首先应该了解SRAM在基于AVR的Arduino中的组织方式(不适用于Intel的Arduino Galileo)。下图来自Adafruit,清楚地表明了这一点:

SRAM组织

然后,您需要知道SRAM的总大小(取决于Atmel MCU,因此取决于您拥有哪种Arduino板)。

在此图上,很容易找出“ 静态数据”块的大小,因为它在编译时是已知的,以后不会更改。

的大小可以知道更多的困难,因为它可以在运行时不同,这取决于动态内存分配(mallocnew由您的草图或使用的库执行)。在Arduino上很少使用动态内存,但是某些标准函数可以做到这一点(String我认为类型会使用它)。

对于堆栈大小,它也会在运行时根据当前函数调用的深度(每个函数调用在堆栈上占用2个字节来存储调用者的地址)以及局部变量的数量和大小(包括传递的参数(到现在为止调用的所有函数也都存储在Stack中

因此,假设您的recurse()函数使用12个字节作为其局部变量和参数,那么对该函数的每次调用(外部调用者的第一个调用和递归调用)将使用12+2字节。

如果我们假设:

  • 您正在使用Arduino UNO(SRAM = 2K)
  • 您的草图不使用动态内存分配(无Heap
  • 您知道静态数据的大小(假设132个字节)
  • recurse()从草图调用函数时,当前堆栈的长度为128个字节

然后,剩下堆栈2048 - 132 - 128 = 1788上的可用字节。因此,对函数的递归调用数为,其中包括初始调用(不是递归调用)。1788 / 14 = 127

如您所见,这很困难,但并非不可能找到想要的东西。

recurse()调用之前获得堆栈可用大小的一种更简单的方法是使用以下函数(位于Adafruit学习中心;我自己尚未对其进行测试):

int freeRam () 
{
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

我强烈建议您在Adafruit学习中心阅读本文


当我写我的书时,我看到peter-r-bloomfield发表了他的回答;他的回答看起来更好,因为它完全描述了调用后堆栈的内容(我已经忘记了寄存器状态)。
jfpoilpret 2014年

两者的质量都很好。
Cyber​​gibbons 2014年

静态数据= .bss + .data,Arduino报告的“全局变量占用的RAM”是什么,对吗?
加布里埃尔·斯台普斯

1
@GabrielStaples完全正确。更详细地.bss表示代码中没有初始值的全局变量,而代表具有初始值data的全局变量。但最后它们使用相同的空间:图表中的静态数据
jfpoilpret

1
@GabrielStaples忘记了一件事,从技术上讲,这些不仅是全局变量,而且static在函数中也声明了变量。
jfpoilpret

8

递归在微控制器上是不好的做法,因为您已经陈述了自己的想法,并且您可能希望尽可能避免它。在Arduino站点上,有一些示例和库可用于检查可用RAM大小。例如,您可以使用它来确定何时中断递归,或者使用一些技巧/风险来勾勒草图并对其进行硬编码。程序中的每个更改以及Arduino工具链中的每个更改都需要此配置文件。


一些更高端的编译器,例如IAR(不支持AVR)和Keil(不支持AVR),具有可帮助您监视和管理堆栈空间的工具。不过,在像ATmega328这样的小型设备上确实不建议这样做。
Cyber​​gibbons 2014年

7

这取决于功能。

每次调用函数时,都会将一个新框架压入堆栈。它通常包含各种关键项目,可能包括:

  • 返回地址(代码中调用该函数的点)。
  • 本地实例指针(this),如果调用成员函数。
  • 参数传递到函数中。
  • 注册功能结束时需要恢复的值。
  • 被调用函数内局部变量的空间。

如您所见,给定调用所需的堆栈空间取决于函数。例如,如果您编写一个仅包含一个int参数且不使用任何局部变量的递归函数,则它在堆栈上不需要的字节数太多。这意味着您可以递归地调用它,而不是使用带有多个参数并使用大量局部变量的函数(这样会更快地耗尽堆栈)。

显然,堆栈的状态取决于代码中发生的其他事情。如果直接在标准loop()函数内启动递归,那么堆栈上可能已经没有太多东西了。但是,如果启动时它在其他功能中嵌套了多个层次,那么空间就不会太大了。这将影响您可以递归多少次而不会耗尽堆栈。

值得注意的是,某些编译器中存在尾部递归优化(尽管我不确定avr-gcc是否支持它)。如果递归调用是函数中的最后一件事,则意味着有时有可能完全避免更改堆栈框架。编译器仅可以重用现有框架,因为“父”调用(可以这么说)已经使用完了。从理论上讲,这将意味着只要您的函数不调用其他任何东西,您就可以根据需要继续进行递归操作。


1
avr-gcc不支持尾递归。
asheeshr 2014年

@AsheeshR-很高兴知道。谢谢。我认为这不太可能。
彼得·布鲁姆菲尔德

您可以通过重构代码来实现尾部调用消除/优化,而不是希望编译器执行此操作。只要递归调用位于递归方法的末尾,您就可以安全地重写该方法以使用while / for循环。
abasterfield 2014年

1
@TheDoctor的帖子与“ avr-gcc不支持尾递归”相矛盾,正如我对其代码的测试一样。编译器确实实现了尾递归,这就是他如何获得一百万次递归。彼得是正确的-编译器可以用jump代替调用/返回(作为函数中的最后一个调用)。它具有相同的最终结果,并且不占用堆栈空间。
尼克·加蒙

2

我有一个完全相同的问题,与我阅读Alex Allain的《跳入C ++》,第16章:递归,第230页,因此我进行了一些测试。

TLDR;

我的Arduino Nano(ATmega328 mcu)在堆栈溢出并崩溃之前可以进行211个递归函数调用(针对下面给出的代码)。

首先,让我解决这个问题:

有时,递归是实现特定算法的唯一快速选择。

[更新:啊,我略读了“快速”一词。在这种情况下,您具有一定的有效性。不过,我认为值得一提。

不,我认为这不是真实的说法。我敢肯定,所有算法无一例外都具有递归和非递归解决方案。只是有时候它要容易得多使用递归算法。话虽这么说,递归在微控制器上的使用却很受限制,并且可能永远不会在安全关键代码中被允许。但是,当然可以在微控制器上进行。要知道您可以使用任何给定的递归函数的深度,只需对其进行测试!在真实的测试案例中,在真实的应用程序中运行它,并删除基本条件,以便无限递归。打印一个计数器,亲自了解您可以走多深,以便您知道递归算法是否将RAM的限制推得太近而无法实际使用。以下是在Arduino上强制堆栈溢出的示例。

现在,请注意以下几点:

您可以获得多少个递归调用或“堆栈帧”取决于许多因素,包括:

  • 您的RAM的大小
  • 堆栈中已经有多少东西或堆中已经占用了多少东西(即:您的可用RAM很重要;free_RAM = total_RAM - stack_used - heap_used或者,您可能会说free_RAM = stack_size_allocated - stack_size_used
  • 每个新的递归函数调用将放置在堆栈上的每个新“堆栈框架”的大小。这取决于调用的函数及其变量和内存要求等。

我的结果:

  • 20171106-2054hrs-东芝Satellite w / 16 GB RAM; 四核,Windows 8.1:崩溃前打印的最终值:43166
    • 花了几秒钟崩溃-可能是5〜10?
  • 20180306-1913hrs带64 GB RAM的戴尔高端笔记本电脑; 8核,Linux Ubuntu 14.04 LTS:崩溃前打印的最终值:261752
    • 后面跟着这句话 Segmentation fault (core dumped)
    • 只花了约4〜5秒时间就崩溃了
  • 20180306-1930hrs Arduino Nano:TBD ---处于〜250000且仍在计数--- Arduino优化设置必须已使它优化了递归... ??? 是的,就是这种情况。
    • 添加#pragma GCC optimize ("-O0")到文件顶部并重做:
  • 20180307-0910hrs Arduino Nano:32 kB闪存,2 kB SRAM,16 MHz处理器或:崩溃前打印的最终值:211 Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
    • 它开始以115200串行波特率打印时仅花费了几分之一秒-可能是1/10秒
    • 2 kiB = 2048字节/ 211堆栈帧= 9.7字节/帧(假设堆栈使用的是所有RAM,实际上不是这种情况)-但这似乎还是很合理的。

代码:

PC应用程序:

/*
stack_overflow
 - a quick program to force a stack overflow in order to see how many stack frames in a small function can be loaded onto the stack before the overflow occurs

By Gabriel Staples
www.ElectricRCAircraftGuy.com
Written: 6 Nov 2017
Updated: 6 Nov 2017

References:
 - Jumping into C++, by Alex Allain, pg. 230 - sample code here in the chapter on recursion

To compile and run:
Compile: g++ -Wall -std=c++11 stack_overflow_1.cpp -o stack_overflow_1
Run in Linux: ./stack_overflow_1
*/

#include <iostream>

void recurse(int count)
{
  std::cout << count << "\n";
  recurse(count + 1);
}

int main()
{
  recurse(1);
}

Arduino“素描”程序:

/*
recursion_until_stack_overflow
- do a quick recursion test to see how many times I can make the call before the stack overflows

Gabriel Staples
Written: 6 Mar. 2018 
Updated: 7 Mar. 2018 

References:
- Jumping Into C++, by Alex Allain, Ch. 16: Recursion, p.230
*/

// Force the compiler to NOT optimize! Otherwise this recursive function below just gets optimized into a count++ type
// incrementer instead of doing actual recursion with new frames on the stack each time. This is required since we are
// trying to force stack overflow. 
// - See here for all optimization levels: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
//   - They include: -O1, -O2, -O3, -O0, -Os (Arduino's default I believe), -Ofast, & -Og.

// I mention `#pragma GCC optimize` in my article here: http://www.electricrcaircraftguy.com/2014/01/the-power-of-arduino.html
#pragma GCC optimize ("-O0") 

void recurse(unsigned long count) // each call gets its own "count" variable in a new stack frame 
{
  // delay(1000);
  Serial.println(count);

  // It is not necessary to increment count since each function's variables are separate (so the count in each stack
  // frame will be initialized one greater than the last count)
  recurse (count + 1);

  // GS: notice that there is no base condition; ie: this recursive function, once called, will never finish and return!
}

void setup()
{
  Serial.begin(115200);
  Serial.println(F("\nbegin"));
  // First function call, so it starts at 1
  recurse (1);
}

void loop()
{
}

参考文献:

  1. Alex Allain跳入C ++,第16章:递归,第230页
  2. http://www.electricrcaircraftguy.com/2014/01/the-power-of-arduino.html-从字面上看:我在此“项目”中引用了自己的网站,以提醒自己如何更改给定文件的Arduino编译器优化级别使用该#pragma GCC optimize命令,因为我知道我已经在其中记录了该命令。

1
请注意,根据avr-lib的文档,在没有优化的情况下,切勿编译依赖avr-libc的任何内容,因为某些事情即使在关闭优化的情况下也无法保证。因此,我建议您不要在#pragma这里使用。相反,您可以添加要取消优化__attribute__((optimize("O0")))单个功能
埃德加·博内特

谢谢,埃德加。您知道AVR libc记录在哪里吗?
加布里埃尔·斯台普斯

1
关于<util / delay.h>文档指出:“为了使这些功能按预期运行,必须启用编译器优化。”(强调原始内容)。我不太确定是否有其他avr-libc函数具有此要求。
埃德加·博内特

1

我写了这个简单的测试程序:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  recurse(1);
}

void loop() {
  // put your main code here, to run repeatedly: 

}

void recurse(long i) {
  Serial.println(i);
  recurse(i+1);
}

我为Uno编译了该代码,在撰写本文时,它已重复执行了100万次以上!我不知道,但是编译器可能已经对该程序进行了优化


在设定的通话次数〜1000之后,尝试返回。那应该会造成问题。
asheeshr 2014年

1
编译器已经巧妙地在草图上实现了尾递归,如您将对其进行反汇编所见。这意味着,它取代了顺序call xxx/ ret通过jmp xxx。除了编译器的方法不消耗堆栈外,这等同于同一件事。因此,您可以使用代码递归数十亿次(其他条件相同)。
尼克·加蒙

您可以强制编译器不优化递归。我会回来并稍后发布示例。
加布里埃尔·斯台普斯

做完了!此处的示例:arduino.stackexchange.com/a/51098/7727。秘诀是通过添加#pragma GCC optimize ("-O0") 到Arduino程序的顶部来防止优化。我相信您必须在要应用到的每个文件的开头执行此操作-但是多年来我没有对此进行过查找,因此请自己研究一下以确保。
加布里埃尔·斯台普斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.