Каковы традиционные способы оптимизации использования программной памяти?

При выполнении крупных проектов с использованием плат 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()
  • ......

Недостаточно памяти, могут возникнуть проблемы со стабильностью.

Насколько опасны эти предупреждения?

Прежде чем пометить его как дубликат, я сослался на следующее. Но это не было удовлетворительным
Самый эффективный способ программирования
Каковы пределы безопасного использования памяти?

, 👍8

Обсуждение

не традиционный способ, но Nano Every имеет в 3 раза больше SRAM, чем классический Nano, и обновленный компилятор не копирует константные строки в SRAM., @Juraj


4 ответа


Лучший ответ:

18

Каковы общепринятые методы оптимизации программы? использование памяти?

Во-первых, обратите внимание, что вы ищете способы уменьшить объем памяти 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


7

Я просто хочу добавить один пункт к превосходному ответу Мишеля Кейзерса:

  • подумайте о каждом элементе, который вы храните в памяти, и спросите себе вопрос: мне действительно нужно держать это в оперативной памяти?

Может показаться глупым говорить о том, что многие считают очевидным, но мы видел здесь много примеров новичков, которые не принимают этого во внимание. рассмотрение. В качестве простого примера рассмотрим эту функцию, которая усредняет 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


4

Какие общепринятые методы оптимизации использования памяти программы?

(примечание. Согласно комментарию Эдгара, я подчеркиваю, что речь идет о более эффективном использовании 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


1

Поскольку вы спросили об традиционных способах, я собираюсь предложить традиционный метод. В данном случае более 50 лет.

Создать и проанализировать список.

Методология:

  1. Скомпилируйте весь код с включенной отладкой (добавьте -g).

  2. Связать код с включенной отладкой, создав исполняемый файл ELF. НЕ преобразовывайте в образ, загружаемый на Arduino.

  3. используйте objdump, чтобы составить список. Мое использование этого чтения:

    ( avr-objdump --headers --source --disassemble --syms program.elf ; \
      avr-objdump --full-contents --section=.final_progmem program.elf ) > program.lst
    

    Ваше использование может варьироваться в зависимости от того, что вы предпочитаете видеть.

Суть в том, что это позволяет вам точно видеть, что использует каждый байт вашей памяти.

Возможно, вы захотите поиграть с различными оптимизациями, пока не поймете, что видите. -O0 обеспечивает наиболее понятную дизассемблирование, а -Os — наименьшую.

Один совет, основанный на этом: библиотеки Arduino предназначены для универсальности, а не для скорости и эффективности использования памяти.

,