Каковы традиционные способы оптимизации использования программной памяти?
При выполнении крупных проектов с использованием плат Arduino (Uno, микроконтроллер Atmega328P). Раньше я получал подобные предупреждения
Sketch uses 13764 bytes (44%) of program storage space. Maximum is 30720 bytes.
Global variables use 1681 bytes (82%) of dynamic memory, leaving 367 bytes for local variables. Maximum is 2048 bytes.
Low memory available, stability problems may occur.
- Каковы общепринятые методы оптимизации использования памяти программы?
- Есть ли разница в использовании памяти, если переменная объявлена глобально или локально.
- Будет ли иметь значение оператор управления или оператор выбора (например,
if
,switch
) - Использование последовательного монитора.
Serial.print()
- ......
Недостаточно памяти, могут возникнуть проблемы со стабильностью.
Насколько опасны эти предупреждения?
Прежде чем пометить его как дубликат, я сослался на следующее. Но это не было удовлетворительным
Самый эффективный способ программирования
Каковы пределы безопасного использования памяти?
@Mayoogh Girish, 👍8
Обсуждение4 ответа
Лучший ответ:
Каковы общепринятые методы оптимизации программы? использование памяти?
Во-первых, обратите внимание, что вы ищете способы уменьшить объем памяти SRAM. Он содержит глобальную (переменную) память и пространство кучи (динамическая память + стековая память).
- Избегайте пробелов в памяти, не используя динамическую память (с помощью free/malloc/new).
- Избегайте использования класса
String
. - Избегайте использования глобальной памяти в SRAM, используя
PROGMEM
,F(..)
, если это возможно. - Используйте наименьший размер переменной (например,
uint8_t
вместоint
). - Сохранять массивы логических значений в битах (8 логических значений на байт).
- Используйте битовые флаги, если применимо.
- Используйте внутренний сжатый тип памяти (влияет на производительность), например, если у вас есть много 6-битных значений для хранения, храните их не в отдельных байтах, а используйте 6 байтов для 8 6-битных значений).
- Избегайте передачи массивов по значению.
- Избегайте большого стека вызовов с множеством (больших) переменных. Обратите внимание, что это влияет на ваш дизайн, поэтому используйте его в крайнем случае.
- Если вам нужна вычисляемая таблица, вычисляйте каждое значение, а не сохраняйте его в виде таблицы.
- Не используйте более длинные массивы, чем необходимо, и подумайте о разумных максимальных размерах массивов в противном случае (см. комментарий hcheung ниже).
- (это не полный список).
Есть ли разница в использовании памяти, если переменная объявлена глобально или локально.
Да, локальные переменные добавляются в стек, но удаляются после завершения функции, глобальные переменные остаются (но создаются только один раз). Обратите внимание, что переменные в стеке (а также динамическая память) НЕ учитываются в памяти, вычисляемой в предупреждающем сообщении во время компиляции.
Будет ли иметь значение оператор управления/выборки? (например, если, переключиться )
Нет, это повлияет только на память программы.
Использование последовательного монитора. Серийный.print()
Возможно, да, последовательный монитор, вероятно, резервирует (вполне?) часть памяти в качестве буфера.
Недостаточно доступной памяти, могут возникнуть проблемы со стабильностью. Насколько опасны эти предупреждения?
Насколько это плохо, зависит от того, сколько используется памяти, которая не рассчитывается, динамической памяти и памяти стека.
Вы можете рассчитать его вручную (что может быть довольно громоздко для большой программы), вы также можете использовать для этого библиотеку GitHub:
Arduino MemoryFree
Если вы знаете, сколько памяти кучи вы используете в худшем случае, добавьте ее в вычисляемую память глобальных переменных. Если это меньше максимально доступной памяти SRAM, вы в безопасности.
Проголосуйте за исчерпывающее резюме. Я хотел бы добавить еще одно «распределение размера массива в соответствии с необходимостью». Я часто вижу, как многие пользователи ArduinoJson создают выделение размером до 1024 байт с документом «StaticJsonDocument<1024>» для целевого объекта данных, состоящего только из пару пар ключ/значение, занимающих не более 50 байт., @hcheung
@hcheung ... хороший ... для меня очевидно, что нельзя использовать слишком много, но с ограниченной системой ОЗУ вы должны тщательно подумать об ограничениях массива., @Michel Keijzers
Как насчет макросов, таких как #define, @Mayoogh Girish
@MayooghGirish #define - это только текстовые замены, результат замены можно применить к примерам, упомянутым в моем списке., @Michel Keijzers
Nitpick: я бы сказал «избегайте использования String», а не «предотвращайте использование String». «Избегать» означает, что вы не должны этого делать. «Предотвратить» означает, что вы должны сделать так, чтобы это не смог сделать кто-то другой или чтобы это не могло произойти само по себе. Точно так же «избегайте хранения *постоянных* глобальных переменных в SRAM» (постоянная часть важна, вы не можете просто переместить *что-либо* в PROGMEM!), @user253751
@ user253751 Вы правы, спасибо, английский не мой родной язык. Я изменю это в своем ответе., @Michel Keijzers
Я сделал еще несколько изменений, на случай, если вы захотите их проверить., @user253751
@user253751 user253751 Все принято, я использовал «скетч», потому что в Arduino это термин «по умолчанию», хотя я предпочитаю программу, поскольку он используется везде., @Michel Keijzers
Программы Arduino называются скетчами, но я думаю, что память программ по-прежнему называется памятью программ, а не памятью скетчей. Например ПРОГРАММА
, @user253751
@user253751 user253751 Верно. Вероятно, Arduino использовал «скетч», поскольку он кажется менее «сложным», чем программа., @Michel Keijzers
Я просто хочу добавить один пункт к превосходному ответу Мишеля Кейзерса:
- подумайте о каждом элементе, который вы храните в памяти, и спросите себе вопрос: мне действительно нужно держать это в оперативной памяти?
Может показаться глупым говорить о том, что многие считают очевидным, но мы видел здесь много примеров новичков, которые не принимают этого во внимание. рассмотрение. В качестве простого примера рассмотрим эту функцию, которая усредняет 500 аналоговых показаний:
int averageAnalogReading()
{
// Сначала возьмите и сохраните показания.
int readings[500];
for (int i = 0; i < 500; i++)
readings[i] = analogRead(inputPin);
// Затем вычислить среднее значение.
long sum = 0;
for (int i = 0; i < 500; i++)
sum += readings[i];
return sum / 500;
}
Сохранение всех этих показаний совершенно бесполезно, так как вы можете просто обновить сумма на лету:
int averageAnalogReading()
{
long sum = 0;
for (int i = 0; i < 500; i++)
sum += analogRead(inputPin);
return sum / 500;
}
По той же причине, если вам нужно какое-то скользящее среднее для сглаживания данные, вам следует рассмотреть возможность использования экспоненциально взвешенного бегущего среднее значение, которое может быть постепенно обновлено без сохранения показания.
Проголосовал, очень хороший момент. Лучший способ улучшить — проверить сами алгоритмы. Краткое примечание: если вас интересуют средние значения, также посмотрите на «скользящие средние», также не требуя дополнительной памяти SRAM для каждого выполненного измерения., @Michel Keijzers
@MichelKeijzers Если вам нужно правильное скользящее среднее с фильтром коробок, вам нужно сохранить последние N измерений. Но если вы можете изменить его на экспоненциальный фильтр, то вы этого не сделаете., @user253751
@user253751 user253751 В этом ты прав. Благодарю за разъяснение., @Michel Keijzers
Какие общепринятые методы оптимизации использования памяти программы?
(примечание. Согласно комментарию Эдгара, я подчеркиваю, что речь идет о более эффективном использовании PROGMEM.)
Если вы можете заменить код таблицей, размер которой ≤ строк кода, сделайте это.
- Вместо последовательности операторов if найдите способ свернуть процедуру в таблицу
- Используйте таблицы указателей на функции, если это имеет смысл
- Иногда можно придумать мини-язык, гораздо более плотный, чем инструкции AVR, например, закодировать логику робота в 16 команд, а затем упаковать две команды на байт. Это может сократить использование памяти в 50 раз.
- Используйте функции вместо повторяющегося кода. Это может показаться очевидным, но часто существуют незаметные способы переписать код (но имейте в виду, что вызовы функций требуют дополнительных затрат).
- Используйте хеш-таблицы, а не таблицы с большими пробелами.
- Используйте фиксированную, а не плавающую точку (например, вы можете взять байт и интерпретировать его значение в диапазоне от 0,00 до 2,55 вместо использования 4-байтового числа с плавающей запятой).
Есть ли разница в использовании памяти, если переменная объявлена глобально или локально.
Давайте поговорим о стеке.
void A() {
byte a[600];
...
}
void B() {
byte b[400];
...
}
void loop() {
byte xxx[1000];
...
}
Поначалу эта программа будет постоянно использовать не менее 1000 байт ОЗУ. Нет никакой реальной разницы по сравнению с объявлением xxx глобально. Но тогда важно, какая функция какую вызовет.
Если loop() вызывает A(), а затем loop() вызывает B(), программа не будет использовать больше 1600 одновременно. Однако если A() вызывает B() или наоборот, программа будет использовать 2000. Для иллюстрации:
loop() [1000]
└──── A() [1600]
│ [1000]
└──── B() [1400]
└──── A() [1600]
└──── B() [1400]
по сравнению с
loop() [1000]
└──── A() [1600]
└──── B() [2000]
│ [1000]
└──── A() [1600]
└──── B() [2000]
Будет ли иметь значение оператор управления или оператор выбора (например, if, switch )
Небольшая разница для небольшого числа случаев. В противном случае это зависит от вашего кода. Лучший способ - просто попробовать оба и посмотреть, какой лучше. Но:
переключатели
обычно используют таблицы переходов, которые довольно компактны, если вы охватываете почти все случаи в диапазоне (0,1,2,3,4,..,100). if
обычно используют последовательность инструкций, которые занимают больше байтов и циклов, чем запись в таблице переходов, но это имеет больше смысла, если у вас нет последовательных отрезков случаев.
Использование последовательного монитора. Серийный.print()
Я не думаю, что это имеет какое-то значение. Последовательные буферы крошечные (скажем, 64 байта или 128 для платы большего размера) и я считаю, что они выделяются независимо от того, используете ли вы Serial или нет.
Конечно, "буквальные строки, подобные этой" и буферы char[] потребляют память. Вы можете закомментировать их (или использовать #ifdef
), когда они вам не нужны.
1. Обратите внимание, что ваши первые 5 пунктов касаются экономии флэш-памяти, а не оперативной памяти. У ОП не было недостатка во вспышке. Таблицы будут на самом деле _cost_ RAM, если вы не поместите их в PROGMEM. 2. По поводу «_Нет никакой реальной разницы по сравнению с объявлением xxx глобально_»: то есть, если только setup()
не требует памяти. 3. Последовательные буферы не выделяются, если вы не используете Serial
., @Edgar Bonet
Спасибо @EdgarBonet 1. Да, я думал о PROGMEM, извините. 2. Не могли бы вы объяснить это? Разве память, выделенная для стека setup()
, не освобождается после ее запуска? 3. Спасибо, я отредактировал это., @Artelius
Объясните пункт 2: setup()
и loop()
обычно не запускаются одновременно, поэтому их использование в стеке не суммируется. Если вы сделаете xxx
глобальным, он будет выделен даже во время работы setup()
. Это не должно вызывать беспокойства, если только setup()
не требует много памяти., @Edgar Bonet
Поскольку вы спросили об традиционных способах, я собираюсь предложить традиционный метод. В данном случае более 50 лет.
Создать и проанализировать список.
Методология:
Скомпилируйте весь код с включенной отладкой (добавьте
-g
).Связать код с включенной отладкой, создав исполняемый файл ELF. НЕ преобразовывайте в образ, загружаемый на Arduino.
используйте
objdump
, чтобы составить список. Мое использование этого чтения:( avr-objdump --headers --source --disassemble --syms program.elf ; \ avr-objdump --full-contents --section=.final_progmem program.elf ) > program.lst
Ваше использование может варьироваться в зависимости от того, что вы предпочитаете видеть.
Суть в том, что это позволяет вам точно видеть, что использует каждый байт вашей памяти.
Возможно, вы захотите поиграть с различными оптимизациями, пока не поймете, что видите. -O0
обеспечивает наиболее понятную дизассемблирование, а -Os
— наименьшую.
Один совет, основанный на этом: библиотеки Arduino предназначены для универсальности, а не для скорости и эффективности использования памяти.
- Как объявить массив переменного размера (глобально)
- Глобальные переменные занимают много места в динамической памяти.
- Оптимизированный генератор случайных буквенно-цифровых строк
- Преобразование int или float в массив байтов в ардуино
- Помогите уменьшить размер скетча!
- Включает ли скомпилированный бинарный файл скетча неиспользуемые функции из библиотеки?
- Приемлемо ли новое без удаления?
- Рефакторинг плохого дизайна?
не традиционный способ, но Nano Every имеет в 3 раза больше SRAM, чем классический Nano, и обновленный компилятор не копирует константные строки в SRAM., @Juraj