Скільки можна повторити? Скільки можна повторити? Скільки ca! @ # QFSD @ $ RFW


19

Дошка Arduino Uno має обмежену оперативну пам'ять, що означає, що у неї обмежений стек викликів. Іноді рекурсія є єдиним швидким варіантом реалізації певного алгоритму. Отже, враховуючи, що стек викликів сильно обмежений, що було б способом з’ясувати, що, маючи певну програму, що працює на дошці, скільки саме рекурсивних дзвінків ви можете собі дозволити, перш ніж відбудеться переповнення стека (і трапляються погані речі)?


2
Чи можете ви навести приклад алгоритму, коли вибір між рекурсивним та нерекурсивним рішенням настільки чіткий?
Кібергіббони

1
@Cybergibbons Останнім часом мені довелося писати та перевіряти RDP за короткий проміжок часу, тому я продовжував рекурсивну реалізацію. Я не кажу, що ітераційний процес неможливий, просто щоб було легше продовжувати рекурсивну версію.
asheeshr

Що це означає в заголовку запитання How much ca!@#QFSD@$RFW:? Мені цікаво, чому ніхто не редагував це щось більш значуще (за останні 4 роки).
Нік Гаммон

Відповідь - це 211часи (залежно від багатьох факторів) :). Дивіться мою відповідь тут: arduino.stackexchange.com/a/51098/7727 . @NickGammon, я думаю, що він робить вигляд, що "проклинає". Це гра на слово для "повторювати". Потрібно мені хвилинку, щоб зрозуміти це теж. Спочатку було досить заплутано.
Габріель Степлес

О Я бачу. Я подумав, що у серверів SE можуть виникнути збої :)
Nick Gammon

Відповіді:


15

Якщо ви дійсно хочете повторити (а як @jippie сказав, що це погана ідея; підсумкове повідомлення: не робіть цього ) і хочете знати, скільки ви можете повторити, то вам доведеться виконати деякі розрахунки та експерименти; також, як правило, ви матимете лише його наближення, оскільки це багато залежить від стану пам'яті під час виклику вашої рекурсивної функції.

Для цього спершу слід знати, як організовано SRAM всередині Arduino на базі AVR (це не стосується, наприклад, Arduino Galileo від Intel). Наведена нижче діаграма Adafruit чітко показує:

Організація SRAM

Тоді вам потрібно знати загальний розмір вашої SRAM (залежить від Atmel MCU, отже, яка у вас Arduino плата).

На цій діаграмі легко з’ясувати розмір блоку статичних даних, оскільки він відомий під час компіляції і згодом не змінюватиметься.

Розмір Heap може бути важче пізнати, оскільки він може змінюватися під час виконання, залежно від динамічного розподілу пам'яті ( 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.


Я бачу, як Пітер-р-Блумфілд розмістив свою відповідь, поки я писав свою; його відповідь виглядає краще, оскільки вона повністю описує вміст стеку після дзвінка (я забув стан регістрів).
jfpoilpret

Обидва дуже якісні відповіді.
Кібергіббони

Статичні дані = .bss + .data, а чи є те, що Arduino повідомляється як "оперативна пам'ять, взята з глобальних змінних" чи що інше, правильно?
Габріель Степлес

1
@GabrielStaples так. Більш докладно .bssпредставляє глобальні змінні без початкового значення у вашому коді, тоді dataяк для глобальних змінних з початковим значенням. Зрештою, вони використовують один і той же простір: Статичні дані на діаграмі.
jfpoilpret

1
@GabrielStaples забув одне, технічно це не тільки глобальні змінні, які там ідуть, у вас також є змінні, оголошені staticв межах функції.
jfpoilpret

8

Рекурсія є поганою практикою на мікроконтролері, як ви вже заявили про себе, і, ймовірно, хочете цього уникати, коли це можливо. На сайті Arduino є кілька прикладів та бібліотек, доступних для перевірки розміру вільного RAM . Наприклад, ви можете використати це, щоб визначити, коли порушити рекурсію, або трохи складніше / ризикувати, щоб профайлювати ваш ескіз і жорсткий код обмеження в ньому. Цей профіль потрібен для кожної зміни вашої програми та для кожної зміни в ланцюзі інструментів Arduino.


Деякі з більш висококласних компіляторів, такі як IAR (які підтримують AVR) і Keil (які не підтримують AVR), мають інструменти, які допоможуть вам контролювати та керувати простором стеку. Насправді не рекомендується робити щось таке маленьке, як ATmega328.
Кібергіббони

7

Це залежить від функції.

Кожен раз, коли викликається функція, на стек висувається новий кадр. Зазвичай він містить різні критичні елементи, потенційно включаючи:

  • Зворотна адреса (точка в коді, з якого викликалася функція).
  • Місцевий покажчик екземпляра ( this), якщо викликає функцію члена.
  • Параметри перейшли у функцію.
  • Зареєструйте значення, які потрібно відновити після завершення функції.
  • Простір для локальних змінних всередині викликаної функції.

Як бачите, простір стеку, необхідний для даного дзвінка, залежить від функції. Наприклад, якщо ви пишете рекурсивну функцію, яка приймає лише intпараметр і не використовує локальних змінних, їй не знадобиться набагато більше, ніж кілька байт на стеці. Це означає, що ви можете рекурсивно називати це набагато більше, ніж функція, яка приймає кілька параметрів і використовує безліч локальних змінних (які з'їдають стек набагато швидше).

Очевидно, що стан стека залежить від того, що ще відбувається в коді. Якщо ви розпочнете рекурсію безпосередньо в рамках стандартної loop()функції, то, ймовірно, на стеку вже не буде багато. Однак якщо ви запустите це вкладене на кілька рівнів глибоке в інших функціях, місця не залишиться стільки. Це вплине на те, скільки разів ви можете повторити, не вичерпуючи стек.

Варто зазначити, що оптимізація хвостової рекурсії існує на деяких компіляторах (хоча я не впевнений, чи підтримує її avr-gcc). Якщо рекурсивний виклик є останнім у функції, це означає, що іноді можливо взагалі уникнути зміни кадру стека. Компілятор може просто повторно використовувати існуючий кадр, оскільки виклик 'батьків' (так би мовити) закінчується його використанням. Це означає, що ви можете теоретично продовжувати повторюватись скільки завгодно, доки ваша функція не вимагає нічого іншого.


1
avr-gcc не підтримує хвостову рекурсію.
asheeshr

@AsheeshR - Добре знати. Спасибі. Я подумав, що це, мабуть, малоймовірно.
Пітер Блумфілд

Ви можете виконувати усунення / оптимізацію хвостових викликів, рефакторинг коду замість того, щоб сподіватися, що компілятор це зробить. Поки рекурсивний виклик знаходиться в кінці рекурсивного методу, ви можете сміливо переписати метод, щоб використовувати цикл while / for.
abasterfield

1
Допис від @TheDoctor суперечить "avr-gcc не підтримує хвостову рекурсію", як і мій тест його коду. Компілятор дійсно реалізував хвостову рекурсію, завдяки чому він отримав до мільйона рекурсій. Петро правильно - компілятор може замінити виклик / повернення (як останній дзвінок у функції) просто перестрибнути . Він має однаковий кінцевий результат і не витрачає місця у стопці.
Нік Гаммон

2

У мене було таке саме запитання, як я читав Алекс Аллайн, стрибки на C ++ , Ch 16: Рекурсія, с.230, тому я провів кілька тестів.

TLDR;

Мій Arduino Nano (ATmega328 mcu) може здійснити 211 рекурсивних викликів функцій (для наведеного нижче коду), перш ніж він переповнюватиме стек та збивається.

По-перше, дозвольте мені вирішити цю скаргу:

Іноді рекурсія є єдиним швидким варіантом реалізації певного алгоритму.

[Оновлення: ах, я знехтував слово "швидкий". У цьому випадку ви маєте деяку силу. Тим не менш, я думаю, що варто сказати наступне.]

Ні, я не думаю, що це правдиве твердження. Я майже впевнений, що всі алгоритми мають без реклюзу і рекурсивне, і нерекурсивне рішення. Просто іноді це значно простішевикористовувати рекурсивний алгоритм. Сказавши це, рекурсія дуже сильно спокушена для використання на мікроконтролерах і, ймовірно, ніколи не буде допущена в критичному для безпеки коді. Тим не менш, це можливо, звичайно, зробити на мікроконтролерах. Щоб знати, наскільки "глибоким" ви можете ввійти в будь-яку задану рекурсивну функцію, просто протестуйте її! Запустіть його у вашому додатку в реальному житті в тестовому випадку реального життя та видаліть базовий стан, щоб він нескінченно повторювався. Роздрукуйте лічильник і переконайтеся, наскільки «глибоко» ви можете піти, щоб ви знали, чи ваш рекурсивний алгоритм підштовхує межі вашої оперативної пам’яті занадто близько, щоб використовувати його практично. Ось приклад нижче, щоб змусити переповнення стека на Arduino.

Тепер кілька приміток:

Скільки рекурсивних дзвінків або «стекових кадрів» можна отримати визначається низкою факторів, включаючи:

  • Розмір оперативної пам’яті
  • Скільки речей вже є у вашому стеку чи зайняте у вашій купі (тобто: ваша безкоштовна оперативна пам'ять має значення; free_RAM = total_RAM - stack_used - heap_usedабо ви можете сказати free_RAM = stack_size_allocated - stack_size_used)
  • Розмір кожного нового «кадру стека», який буде розміщений на стеку для кожного нового рекурсивного виклику функції. Це залежатиме від викликаної функції та її змінних та вимог до пам'яті тощо.

Мої результати:

  • 20171106-2054год - Toshiba Satellite w / 16 ГБ оперативної пам’яті; чотирьохядерний, Windows 8.1: остаточне значення, надруковане перед збоєм : 43166
    • на аварію знадобилося кілька секунд - можливо, 5 ~ 10?
  • 20180306-1913год Dell високого класу ноутбука з 64 ГБ оперативної пам’яті; 8-ядерний, Linux Ubuntu 14.04 LTS: остаточне значення надруковано перед збоєм : 261752
    • слідом за фразою Segmentation fault (core dumped)
    • Аварія займала лише ~ 4 ~ 5 сек
  • 20180306-1930год. Arduino Nano: TBD --- знаходиться на рівні ~ 250000 і все ще підраховує --- налаштування оптимізації Arduino, мабуть, спричинили його оптимізацію рекурсії ... ??? Так, саме так.
    • Додати #pragma GCC optimize ("-O0")вгору файлу і повторити:
  • 20180307-0910 годин Arduino Nano: спалах 32 кБ, 2 кБ SRAM, процесор 16 МГц: кінцеве значення надруковано перед збоєм: 211 Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
    • щойно вона почала друкувати зі швидкістю передачі 115200 серійних передач у секунду - може бути лише 1/10 сек
    • 2 кіБ = 2048 байт / 211 кадрів стека = 9,7 байт / кадр (якщо припустити, що всі ваші оперативні пам'яті використовуються стеком - що насправді не так) - але це здається дуже розумним.

Кодекс:

Додаток для ПК:

/*
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. Стрибки на 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, і, як я пишу, він повторювався понад 1 мільйон разів! Я не знаю, але компілятор, можливо, оптимізував цю програму


Спробуйте повернутися після встановленої кількості дзвінків ~ 1000. Тоді це повинно створити проблему.
asheeshr

1
Компілятор хитро реалізував хвостову рекурсію на вашому ескізі, як ви побачите, чи розібрали її. Це означає, що він замінює послідовність call xxx/ retна jmp xxx. Це означає те саме, за винятком того, що метод компілятора не використовує стек. Таким чином, ви могли повторити мільярди разів зі своїм кодом (за інших рівних умов).
Нік Гаммон

Ви можете змусити компілятор не оптимізувати рекурсію. Я повернусь і опублікую приклад пізніше.
Габріель Степлес

Готово! Приклад тут: arduino.stackexchange.com/a/51098/7727 . Секрет полягає в тому, щоб запобігти оптимізації, додавши #pragma GCC optimize ("-O0") до верхньої частини програми Arduino. Я вважаю, що ви повинні зробити це у верхній частині кожного файлу, до якого ви хочете, щоб він застосував - але я не шукав цього в роках, тому досліджуйте це на собі, щоб бути впевненим.
Габріель Степлес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.