View Categories

Оптимизация памяти

Чаще всего мы сталкиваемся с нехваткой памяти: постоянной Flash или оперативной RAM. После компиляции кода мы получаем сообщение о занимаемом объёме Flash/RAM, это ценная информация. Flash память можно забивать на 99%, её объём не изменяется в процессе работы устройства, чего не скажешь о RAM. Допустим, на момент запуска программы у нас занято 80% оперативной памяти глобальными переменными, но в процессе работы могут появляться и исчезать локальные и динамические переменные, которые добьют занимаемый объём до 100% и устройство скорее всего перезагрузится или зависнет. Опасность ещё в том, что при динамическом выделении памяти могут оставаться пустые места и объём занятой памяти будет увеличиваться ещё быстрее.

В следующем уроке рассмотрим оптимизацию скорости выполнения программы.

С чем компилятор справится сам #

Компилятор оптимизирует действия с переменными, которые не помечены как volatile, так как это прямая команда "не оптимизируй меня". Это важный момент, потому что действия с такими переменными (если они нужны) надо оптимизировать вручную. Компилятор не будет оптимизировать вычисления, вырезать неиспользуемые переменные и конструкции с их применением!

Вырезание переменных и функций #

Компилятор вырезает из кода переменные, а также реализацию функций и методов класса, если они не используются в коде. Таким образом даже если мы подключаем огромную библиотеку, но используем из неё лишь пару методов, объём памяти не увеличится на размер всей библиотеки. Компилятор возьмёт только то, что используется.

Вырезание условий и свитчей #

Компилятор вырежет целую ветку условий или свитчей, если заранее будет уверен в результате сравнения или выбора. Как его в этом убедить? Правильно, константой! Рассмотрим элементарный пример - условие или свитч с тремя вариантами:

switch (num) {
    case 0: Serial.println("Hello 0"); break;
    case 1: Serial.println("Hello 1"); break;
    case 2: Serial.println("Hello 2"); break;
}

// или

if (num == 0) Serial.println("Hello 0");
else if (num == 1) Serial.println("Hello 1");
else if (num == 2) Serial.println("Hello 2");

Если объявить num как обычную переменную - в скомпилированный код попадёт вся конструкция целиком, три условия или весь свитч. Если num сделать константой const или дефайном #define - компилятор вырежет весь блок условий или свитч и оставит только содержимое, которое получается при заданном num. В этом очень легко убедиться, скомпилировав код и посмотрев на объём занимаемой памяти в логе компилятора. При помощи данного трюка можно ускорить выполнение некоторых функций и уменьшить занимаемое ими место в памяти, например для создания универсальной библиотеки.

Если вы пишете свою библиотеку или класс, то всё будет чуть труднее: константы внутри класса не являются для компилятора весомым поводом для вырезания условий и свитчей, даже если это const и он объявлен в списке инициализации класса. Для того, чтобы компилятор вырезал условие или свитч внутри реализации методов класса, ему нужна внешняя константа/дефайн или шаблон template.

Измерение памяти #

Можно использовать мою библиотеку или код из неё. Пример:

#include <Benchmark.h>

void setup() {
    Serial.begin(115200);
    Serial.println(getFreeHeap());

    int* p = new int[100];
    Serial.println(getFreeHeap());
    delete [] p;
}
void loop() {}

Оптимизация #

Использовать переменные соответствующих типов #

Просто не используйте переменные более тяжёлых типов там, где это не нужно.

Использовать define #

Для хранения констант в стиле номеров пинов, каких-то настроек и постоянных значений используйте не глобальные переменные, а #define. Таким образом константа будет храниться в коде программы, во Flash памяти, которой много:

#define MOTOR_PIN 10
#define MOTOR_SPEED 120

Использовать директивы препроцессора #

Если у вас какой-то комплексный проект, где перед прошивкой включаются/выключаются некоторые куски кода или библиотеки - используйте условную компиляцию при помощи директив #if, #elif, #ifdef и прочих, подробнее - в уроке.

Использовать PROGMEM #

Для хранения больших объемов постоянных данных (массив битмапов для вывода на дисплеи, строки с текстом, "таблицы" синуса или других корректирующих значений) используйте PROGMEM - возможность хранить и читать данные во Flash памяти микроконтроллера, которой гораздо больше, чем оперативной. Особенность состоит в том, что данные во Flash пишутся во время прошивки, и изменить их потом будет нельзя, можно только прочитать и использовать. Читайте подробный урок по PROGMEM.

Использовать F() макро #

Если в проекте используется вывод в COM порт фиксированных текстовых данных, то каждый символ будет занимать один байт оперативной памяти, также это относится к строковым данным и выводам на дисплей. У нас есть на вооружении встроенный инструмент, который позволяет хранить строки во Flash памяти, использовать его удобнее того же PROGMEM. Работает очень просто и эффективно, позволяя делать девайс с расширенным общением/отладкой через Serial порт и не думать о забитой оперативке:

// данный вывод (строка, текст) занимает в оперативной памяти 18 байт
Serial.println("Hello <username>!");

// данный вывод ничего не занимает в оперативной памяти, благодаря F()
Serial.println(F("Type /help to help"));

Не использовать float #

Как мы обсуждали в уроке про типы данных, поддержка вычислений с float является программной (для AVR), то есть грубо говоря для вычислений "подключается библиотека". Однократно использовав в коде все арифметические действия с float, вы подключите около 1000 байт кода во Flash память для поддержки этих вычислений. Также продублирую пример из предыдущей главы: если нужно хранить много float значений в оперативной или EEPROM памяти, то есть смысл заменить их целочисленными. Как это сделать без потери точности:

// допустим, нам нужно хранить массив таких значений, не тратя лишнюю память.
// пусть sensorRead() возвращает float температуру с точностью до 1 знака.
// Превратим её в целочисленное, умножив на 10 (или 100, смотря какая нужна точность):
vals[30] = sensorRead() * 10;

// целочисленные int vals занимают в два раза меньше памяти!
// Чтобы превратить их обратно во float - просто делим на 10
float val_f = vals[30] / 10.0;

Не использовать динамическую память #

Использование инструментов динамического выделения памяти (new, malloc...) подключает "библиотеку" для работы с памятью, она довольно тяжёлая.

Не использовать объекты классов Serial и String #

Пожалуй самые "жирные" по занимаемой памяти библиотеки - это стандартные объекты Serial и String. Если в коде появляется Serial, он сразу же забирает себе минимум 998 байт Flash (3% для ATmega328) и 175 байт SRAM (8% для ATmega328). Как только начинаем использовать строки String - прощаемся с 1178 байтами Flash (4% для ATmega328).

Использовать однобитные флаги #

Логический тип bool занимает 1 байт, а не 1 бит. Но флаги можно паковать в байты и другие типы данных, читайте в отдельном уроке.

Отказаться от стандартной инициализации #

Стандартные функции setup() и loop() являются обязательными не просто так: они входят в самую главную функцию всей программы - int main(). Реализация данной функции лежит в ядре в файле main.cpp и выглядит примерно вот так:

// main.cpp

int main() {
    // системный код
    setup();

    for (;;) {
        // системный код
        loop();
    }
}

Именно здесь, в инициализациях, кроется пара сотен байт занимаемой Flash памяти! А после loop() есть проверка условия, которое чуть замедляет основной цикл.

В функциях инициализации настраивается периферия микроконтроллера: АЦП, интерфейсы, таймер 0 (который даёт нам корректный millis()), и некоторые другие вещи. Если можете самостоятельно инициализировать только нужную периферию - это позволит сэкономить несколько сотен байт флэша, всё что нужно сделать - это нагло ввести в скетч свою функцию int main() и написать инициализацию только того, что нужно. Для сравнения: стандартный набор инициализации (функции setup() и loop() в скетче) дают 444 байта занимаемой Flash (Arduino IDE v. 1.8.9). Если отказаться от этого кода и перехватить управление main() - пустой скетч будет занимать 134 байта, что почти на 300 байт меньше! Это, конечно, крохоборство, но на дороге не валяется. Как это сделать:

#include <Arduino.h>

int main() {
    // наш личный "setup"
        for (;;) {
            // наш личный "loop"
        }
    return 0;
}
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх