재발 할 수 있습니까? 재발 할 수 있습니까? 얼마나 많은 ca! @ # QFSD @ $ RFW


19

Arduino Uno 보드에는 RAM이 제한되어 있으므로 사용 가능한 호출 스택이 제한적입니다. 때로는 재귀가 특정 알고리즘을 구현하는 유일한 빠른 옵션 인 경우가 있습니다. 따라서 호출 스택이 심각하게 제한되어 있기 때문에 보드에서 실행되는 특정 프로그램에서 스택 오버플로가 발생하기 전에 얼마나 많은 재귀 호출을 감당할 수 있는지 알 수있는 방법은 무엇입니까?


2
재귀 솔루션과 비 재귀 솔루션 사이의 선택이 분명한 알고리즘의 예를 들어 줄 수 있습니까?
Cybergibbons

1
@Cybergibbons 가장 최근 에는 짧은 시간 내에 RDP 를 작성하고 테스트 해야했기 때문에 재귀 구현을 진행했습니다. 반복 할 수 없다고 말하는 것이 아니라 재귀 버전으로 진행하는 것이 더 쉬울 수 있다는 것입니다.
asheeshr

질문 제목에서 이것은 무엇을 의미 How much ca!@#QFSD@$RFW합니까? 왜 아무도 지난 4 년 동안 더 의미있는 것으로 편집하지 않았는지 궁금합니다.
Nick Gammon

답은 211시간입니다 (많은 요소에 따라 다름) :). 내 대답은 arduino.stackexchange.com/a/51098/7727 에서 참조하십시오 . @NickGammon, 그는 "저주"척 생각합니다. "재귀"에 대한 단어 놀이입니다. 저도 알아 내기 위해 잠시 걸렸습니다. 처음에는 꽤 혼란 스러웠습니다.
가브리엘 스테이플

아 알 겠어요 나는 SE 서버에 결함이 있다고 생각했다 :)
Nick Gammon

답변:


15

정말로 재귀를 원하고 (@jippie가 말한 것처럼 나쁜 생각입니다. 잠재 의식 메시지 : 하지 마십시오 ) 얼마나 많은 재귀를 할 수 있는지 알고 싶다면 계산과 실험을 수행해야합니다. 또한 재귀 함수가 호출 될 때 메모리 상태에 크게 의존하기 때문에 일반적으로 근사치입니다.

이를 위해 먼저 SVR이 AVR 기반 Arduino 내에서 어떻게 구성되는지 알아야합니다 (예 : Intel의 Arduino Galileo에는 적용되지 않음). Adafruit 의 다음 다이어그램 은이를 명확하게 보여줍니다.

SRAM 조직

그런 다음 SRAM의 전체 크기를 알아야합니다 (Atmel MCU에 따라 어떤 종류의 Arduino 보드가 있는지).

이 다이어그램에서는 컴파일 타임에 알려진 정적 데이터 블록의 크기를 쉽게 찾을 수 있으며 나중에 변경되지 않습니다.

은 런타임에 달라질 수있는 크기는 동적 메모리 할당 (에 따라, 알고하는 것이 더 어려울 수 있습니다 malloc또는 new스케치하거나 사용하는 라이브러리에 의해 수행). Arduino에서는 동적 메모리를 사용하는 것이 매우 드물지만 일부 표준 기능이 사용합니다 (유형이 String사용합니다).

를 들어 스택의 크기, 또한 함수 호출의 현재의 깊이에 따라, 런타임 동안 다양하고 전달 된 인수를 포함하는 지역 변수의 수와 크기 ((각 함수 호출은 호출자의 주소를 저장하기 위해 스택에 2 바이트 소요) 지금까지 호출 된 모든 함수 에 대해 스택 에도 저장됩니다 .

따라서 recurse()함수가 로컬 변수와 인수에 12 바이트를 사용하고이 함수 (외부 호출자의 첫 번째 함수와 재귀 함수)를 호출 할 때마다 바이트를 사용 한다고 가정 해 봅시다 12+2.

우리가 가정한다면 :

  • Arduino UNO에 있습니다 (SRAM = 2K).
  • 스케치에서 동적 메모리 할당을 사용하지 않습니다 ( 없음 ).
  • 정적 데이터 의 크기를 알고 있습니다 (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

둘 다 아주 좋은 품질의 답변입니다.
Cybergibbons

정적 데이터 = .bss + .data이며 Arduino에서 "전역 변수가 차지하는 RAM"또는 다른 것으로보고 된 것은 무엇입니까?
Gabriel Staples 5

1
@GabrielStaples 맞습니다. 보다 자세하게 .bss는 코드에 초기 값이없는 전역 변수를 나타내며 초기 값이있는 전역 변수를 나타냅니다 data. 그러나 결국 그들은 같은 공간을 사용합니다 : 다이어그램의 정적 데이터 .
jfpoilpret

1
@GabrielStaples는 기술적으로 전역 변수 일뿐 만 아니라 static함수 내에 선언 된 변수도 있습니다 .
jfpoilpret

8

이미 언급했듯이 마이크로 컨트롤러에서 재귀는 나쁜 습관이며 가능할 때마다 피하는 것이 좋습니다. 에 아두 이노 사이트 무료 RAM의 크기를 확인하기위한 가능한 몇 가지 예와 라이브러리가있다 . 예를 들어 스케치를 프로파일 링하고 한계를 하드 코딩하기 위해 재귀 또는 비트 트릭 / 리 스키어를 중단 할시기를 파악하기 위해이 정보를 사용할 수 있습니다. 이 프로파일은 프로그램의 모든 변경과 Arduino 툴 체인의 모든 변경에 필요합니다.


IAR (AVR을 지원하는) 및 Keil (AVR을 지원하지 않는)과 같은 일부 고급 컴파일러에는 스택 공간을 모니터링하고 관리하는 데 유용한 도구가 있습니다. ATmega328만큼 작은 것에 대해서는 권장하지 않습니다.
Cybergibbons

7

기능에 따라 다릅니다.

함수가 호출 될 때마다 새 프레임이 스택으로 푸시됩니다. 일반적으로 다음을 포함하여 다양한 중요 항목이 포함됩니다.

  • 리턴 주소 (함수가 호출 된 코드의 지점).
  • this멤버 함수를 호출하는 경우 로컬 인스턴스 포인터 ( )
  • 함수에 전달 된 매개 변수
  • 기능이 종료 될 때 복원해야하는 값을 등록하십시오.
  • 호출 된 함수 내의 지역 변수를위한 공간.

보시다시피, 주어진 호출에 필요한 스택 공간은 기능에 따라 다릅니다. 예를 들어 int매개 변수 만 사용하고 로컬 변수를 사용하지 않는 재귀 함수를 작성 하면 스택에서 몇 바이트 이상을 필요로하지 않습니다. 즉, 여러 매개 변수를 사용하고 많은 지역 변수를 사용하는 함수보다 훨씬 더 재귀 적으로 호출 할 수 있습니다 (스택을 훨씬 빨리 소모합니다).

분명히 스택의 상태는 코드에서 다른 일이 일어나고 있는지에 달려 있습니다. 표준 loop()함수 내에서 직접 재귀를 시작하면 이미 스택에 많은 것이 없을 것입니다. 그러나 다른 기능에 여러 수준의 중첩을 시작하면 공간이 충분하지 않습니다. 스택을 소진하지 않고 재발 할 수있는 횟수에 영향을줍니다.

꼬리 재귀 최적화는 일부 컴파일러에 존재한다는 점에 주목할 가치가 있습니다 (avr-gcc가 그것을 지원하는지 확실하지는 않지만). 재귀 호출이 함수의 마지막 항목이면 스택 프레임을 전혀 변경하지 않는 것이 때때로 가능하다는 것을 의미합니다. 컴파일러는 기존 프레임을 재사용 할 수 있습니다. '부모'호출 (말하기)이 사용을 마치기 때문입니다. 즉, 함수가 다른 것을 호출하지 않는 한 이론적으로 원하는만큼 계속 반복 할 수 있습니다.


1
avr-gcc는 꼬리 재귀를 지원하지 않습니다.
asheeshr

@AsheeshR-알아두면 좋습니다. 감사. 아마 그럴 것 같지 않다.
Peter Bloomfield

컴파일러가 코드를 처리하기를 기대하는 대신 코드를 리팩토링하여 테일 콜 제거 / 최적화를 수행 할 수 있습니다. 재귀 호출이 재귀 메서드의 끝에있는 한 while / for 루프를 사용하도록 메서드를 안전하게 다시 작성할 수 있습니다.
abasterfield

1
@TheDoctor의 게시물은 그의 코드 테스트와 마찬가지로 "avr-gcc는 꼬리 재귀를 지원하지 않습니다"와 모순됩니다. 컴파일러는 실제로 테일 재귀를 구현했는데, 이것이 백만 재귀를 얻는 방법입니다. Peter는 정확합니다. 컴파일러가 call / return (함수의 마지막 호출)을 간단히 jump 로 대체 할 수 있습니다. 최종 결과는 동일하며 스택 공간을 소비하지 않습니다.
Nick Gammon

2

Alex Allain , Ch 16 : Recursion, p.230의 C ++로 점프를 읽는 것과 똑같은 질문이 있었 으므로 테스트를 실행했습니다.

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-16GB RAM의 Toshiba Satellite; 쿼드 코어, Windows 8.1 : 충돌 전 최종 값 인쇄 : 43166
    • 충돌하는 데 몇 초가 걸렸을 것입니다.
  • 20180306-1913 시간 Dell 고급 노트북 (64GB 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 : 32kB 플래시, 2kB SRAM, 16MHz 프로세스 또는 충돌 전 최종 값 인쇄 : 211 Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
    • 115200 직렬 전송 속도 (1/10 초)로 인쇄를 시작하면 1 초만에
    • 2 kiB = 2048 bytes / 211 stack frames = 9.7 bytes / frame (모든 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);
}

아두 이노 "스케치"프로그램 :

/*
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 ++로 점프 , Ch 16 : 재귀, p.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")))단일 기능에 추가 할 수 있습니다 .
Edgar Bonet

고마워, 에드거 AVR libc에이 문서가 어디에 있는지 알고 있습니까?
Gabriel Staples

1
<util / delay.h>에 대한 문서는 “이러한 기능이 의도 한대로 작동 하려면 컴파일러 최적화 활성화 해야 합니다 [...]”(원래 강조). 다른 avr-libc 함수 에이 요구 사항이 있는지 확실하지 않습니다.
Edgar Bonet

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);
}

나는 우노를 위해 그것을 컴파일했고, 글을 쓸 때 백만 번 이상 되풀이되었습니다! 잘 모르겠지만 컴파일러가이 프로그램을 최적화했을 수도 있습니다


설정된 횟수만큼 ~ 1000 번 전화를 한 후 돌아 오십시오. 그러면 문제가 발생합니다.
asheeshr

1
컴파일러는 스케치를 분해하면 교묘하게 꼬리 재귀 를 구현 했습니다. 이것이 의미하는 바는 시퀀스를 call xxx/ ret로 대체한다는 것 jmp xxx입니다. 이것은 컴파일러의 메소드가 스택을 소비하지 않는다는 점을 제외하고는 같은 것입니다. 따라서 코드로 수십억 번 반복 할 수 있습니다 (다른 것들은 동일 함).
Nick Gammon

컴파일러가 재귀를 최적화하지 않도록 할 수 있습니다. 나중에 다시 예제를 게시하겠습니다.
Gabriel Staples 1

끝난! 여기 예 : arduino.stackexchange.com/a/51098/7727 . 비밀은 #pragma GCC optimize ("-O0") Arduino 프로그램 상단 에 추가 하여 최적화를 방지하는 것입니다. 적용하고자하는 파일 의 맨 위에서이 작업을 수행해야한다고 생각합니다. 그러나 몇 년 동안 검색하지 않았으므로 확실하게 조사하십시오.
가브리엘 스테이 플스
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.