Плата Arduino Uno имеет ограниченную оперативную память, что означает, что она имеет ограниченный стек вызовов. Иногда рекурсия является единственным быстрым вариантом реализации определенного алгоритма. Итак, учитывая, что стек вызовов строго ограничен, как можно было бы узнать, сколько именно рекурсивных вызовов вы можете себе позволить, учитывая определенную программу, запущенную на плате, до того, как произойдет переполнение стека (и произойдут плохие вещи)?
programming
sram
asheeshr
источник
источник
How much ca!@#QFSD@$RFW
? Мне любопытно, почему никто не отредактировал это как нечто более значимое (за последние 4 года).211
раз (в зависимости от многих факторов) :). Смотрите мой ответ здесь: arduino.stackexchange.com/a/51098/7727 . @NickGammon, он притворяется "проклятым", я думаю. Это игра слов "рекурс". Мне понадобилась минутка, чтобы понять это тоже. Сначала было довольно странно.Ответы:
Если вы действительно хотите выполнить рекурсию (и, как сказал @jippie, это плохая идея; подсознательное сообщение: не делайте этого ) и хотите узнать, сколько вы можете рекурсировать, вам придется выполнить некоторые вычисления и эксперименты; Кроме того, вы, как правило, будете иметь только приблизительное значение, так как оно сильно зависит от состояния памяти в момент вызова рекурсивной функции.
Для этого вам следует сначала узнать, как организована SRAM в Arduino на базе AVR (это не относится, например, к Arduino Galileo от Intel). Следующая диаграмма от Adafruit ясно показывает это:
Затем вам нужно знать общий размер вашей SRAM (зависит от микроконтроллера Atmel, следовательно, какая у вас плата Arduino).
На этой диаграмме легко определить размер блока статических данных, так как он известен во время компиляции и не изменится позже.
Размер кучи может быть сложнее узнать, так как он может изменяться во время выполнения, в зависимости от динамического выделения памяти (
malloc
илиnew
), выполняемого вашим эскизом или используемыми библиотеками. Использование динамической памяти довольно редко в Arduino, но некоторые стандартные функции делают это (String
я думаю, что это использует тип ).Что касается размера стека , он также будет меняться во время выполнения, в зависимости от текущей глубины вызовов функций (каждый вызов функции занимает 2 байта в стеке для хранения адреса вызывающего абонента) и количества и размера локальных переменных, включая переданные аргументы ( которые также хранятся в стеке ) для всех функций, вызываемых до сих пор.
Итак, давайте предположим, что ваша
recurse()
функция использует 12 байтов для своих локальных переменных и аргументов, тогда каждый вызов этой функции (первый из внешнего и рекурсивного) будет использовать12+2
байты.Если мы предположим, что:
recurse()
функция вызывается из вашего эскиза, текущий стек имеет длину 128 байтТогда у вас останутся
2048 - 132 - 128 = 1788
доступные байты в стеке . Таким образом, количество рекурсивных вызовов вашей функции1788 / 14 = 127
, включая начальный вызов (который не является рекурсивным).Как видите, это очень сложно, но не невозможно найти то, что вы хотите.
Более простой способ получить размер стека, доступный до
recurse()
вызова, - использовать следующую функцию (найдена в учебном центре Adafruit; я сам не проверял):Я настоятельно рекомендую вам прочитать эту статью в учебном центре Adafruit.
источник
.bss
представляет глобальные переменные без начального значения в вашем коде, тогдаdata
как для глобальных переменных с начальным значением. Но в конце они используют одно и то же пространство: статические данные на диаграмме.static
внутри функции.Рекурсия - плохая практика на микроконтроллере, поскольку вы уже заявили о себе и, возможно, захотите избежать ее, когда это возможно На сайте Arduino есть несколько примеров и библиотек, доступных для проверки размера свободной оперативной памяти . Например, вы можете использовать это, чтобы выяснить, когда нужно прервать рекурсию, или немного сложнее / рискованнее, чтобы профилировать ваш эскиз и жестко закодировать ограничение в нем. Этот профиль будет необходим для каждого изменения в вашей программе и для каждого изменения в цепочке инструментов Arduino.
источник
Это зависит от функции.
Каждый раз, когда вызывается функция, новый кадр помещается в стек. Обычно он содержит различные критические элементы, в том числе:
this
) при вызове функции-члена.Как видите, объем стека, необходимый для данного вызова, зависит от функции. Например, если вы пишете рекурсивную функцию, которая принимает только
int
параметр и не использует локальные переменные, ей не потребуется намного больше, чем несколько байтов в стеке. Это означает, что вы можете рекурсивно вызывать его гораздо чаще, чем функцию, которая принимает несколько параметров и использует много локальных переменных (которые будут поглощать стек намного быстрее).Очевидно, что состояние стека зависит от того, что еще происходит в коде. Если вы запустите рекурсию непосредственно в стандартной
loop()
функции, то в стеке, вероятно, уже не будет много. Однако, если вы запустите его, вложив несколько уровней в другие функции, места не будет. Это будет влиять на то, сколько раз вы можете выполнить рекурсию, не исчерпав стека.Стоит отметить, что оптимизация хвостовой рекурсии существует на некоторых компиляторах (хотя я не уверен, поддерживает ли она avr-gcc). Если рекурсивный вызов является самой последней вещью в функции, это означает, что иногда можно вообще избежать изменения кадра стека. Компилятор может просто повторно использовать существующий фрейм, так как «родительский» вызов (так сказать) закончил его использование. Это будет означать, что теоретически вы можете продолжать использовать столько раз, сколько захотите, если ваша функция не вызывает ничего другого.
источник
У меня был точно такой же вопрос, когда я читал « Прыжок в C ++» Алекса Аллена , гл. 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
)Мои результаты:
Segmentation fault (core dumped)
#pragma GCC optimize ("-O0")
в начало файла и повторите:Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
Код:
Приложение для ПК:
Программа Arduino "Эскиз":
Ссылки:
#pragma GCC optimize
командой, так как я знал, что я это задокументировал.источник
#pragma
вы используете там. Вместо этого, вы можете добавить__attribute__((optimize("O0")))
к одной функции , которую вы хотите unoptimize.Я написал эту простую тестовую программу:
Я скомпилировал его для Uno, и, когда я пишу, это повторялось более миллиона раз! Я не знаю, но компилятор, возможно, оптимизировал эту программу
источник
call xxx
/ret
наjmp xxx
. Это равносильно тому, что метод компилятора не использует стек. Таким образом, вы можете повторять миллиарды раз с вашим кодом (при прочих равных условиях).#pragma GCC optimize ("-O0")
в начало вашей программы Arduino. Я считаю, что вы должны делать это в верхней части каждого файла, к которому вы хотите, чтобы он применялся, - но я не проверял это годами, поэтому исследуйте его для себя, чтобы быть уверенным.