Оптимизация кода для микроконтроллеров
Существует множество способов оптимизации программ для МК, большая часть из которых следует напрямую из того, как данные хранятся и обрабатываются в микроконтроллере. Самое очевидное, с чего следует начинать оптимизацию это, разумеется использование под данные типов, соответствующим реальным диапазонам данных, которые в них могут хранятся — например, очевидно, нет смысла под хранение целочисленных значений температуры отводить int32, поскольку старшие биты никогда не будут использованы, а память займут. Это буквально основы программирования для встраиваемых устройств, поэтому здесь это рассматриваться не будет.
Разберём подробнее отдельные методы оптимизации, которые применяются постоянно, но в подробностях описываются редко.
Иногда пишут, что inline указывает компилятору, что функция должна быть встроенной. Директива фактически говорит компилятору поместить новую копию кода функции в каждое место, где она вызывается. Код со встроенными функциями работает немного быстрее обычного, поскольку пропадают дополнительные действия по вызову функций, однако же при этом увеличивается расход памяти. Встраивание функций аналогично по сути подстановке из макроса, но в отличие от неё не изменяет исходный код и происходит во время компиляции, в то время как макросы изменяют исходный код перед компиляцией.
Нужно иметь ввиду, что многие современные компиляторы могут игнорировать такие "подсказки".
Встраивание это важная оптимизация, однако оказываемый эффект на производительность может быть различным. Как правило, некоторое количество встраиваний помогает улучшить быстродействие при очень незначительных затратах пространства, но избыточность использования может замедлить программу из-за того, что встроенный код станет потреблять слишком много кэша инструкций, а также займёт значительное количество пространства.
Ключевое слово inline говорит линковщику, что эта функция будет определена в разных единицах трансляции, игнорировать это. Линковщик должен убедится, что все единицы трансляции используют один и тот же экземпляр.
Когда следует использовать ключевое слово inline для функции или метода в C\C++?
Практически только в случаях когда вы хотите определить функцию в заголовочом файле. Точнее, когда более одного определения функции встречается в различных единицах трансляции. Будет хорошей идеей поместить маленькие (однострочные) функции в заголовок (файл .h), так как это даст больше информации для оптимизации компилятору. Однако, это увеличит время сборки.
Когда не следует использовать ключевое слово inline для функции или метода в C\C++?
Не используйте inline только ради ощущения, что ваш код заработает быстрее.
Когда компилятор не знает, что нужно сделать функцию встроенной?
В целом, компилятор умеет выполнять такую оптимизацию лучше вас. Однако, он не сможет это сделать, если нет определения функции (в данной единице трансляции). Как правило, в максимально оптимизированном коде все private методы встраиваются, просили вы того, или нет.
Для предотвращения встраивания в GCC используйте __attribute__(( noinline )), в Visual Studio __declspec(noinline).
Все три ключевых слова (inline, static и extern) используются в основном линковщиком, а не компилятором.
По умолчанию строковые литералы (как "Test. Hello world!") размещаются в SRAM, даже если они константны. Это тратит ценную SRAM, которая нужна для переменных, стека и т.д. Например:
PSTR это инструкция используемая для оптимизации памяти в микроконтроллерах AVR, где SRAM (оперативная память) ограничена, а flash (программная память) больше и используется для кода/констант.
Чтобы хранить строку только во flash и экономить SRAM, используйте макрос F() (для простоты) или PSTR с функциями чтения из PROGMEM.
Плюсы: Экономит SRAM для большего количества строк или других данных.
Минусы: Доступ медленнее, нельзя модифицировать строки во flash. Работает только с константными строками.
Данная оптимизация релевантна для AVR-based MCU (Arduino Uno, Nano, Mega на ATmega), где SRAM мала (0.5–8 КБ), а flash 16–256 КБ.
Для других платформ:
Разберём подробнее отдельные методы оптимизации, которые применяются постоянно, но в подробностях описываются редко.
Ключевое слово inline
Встраивание функций (англ. Inlining) —
способ оптимизации, при котором вызов функции заменяется непосредственно её телом.
Иногда пишут, что inline указывает компилятору, что функция должна быть встроенной. Директива фактически говорит компилятору поместить новую копию кода функции в каждое место, где она вызывается. Код со встроенными функциями работает немного быстрее обычного, поскольку пропадают дополнительные действия по вызову функций, однако же при этом увеличивается расход памяти. Встраивание функций аналогично по сути подстановке из макроса, но в отличие от неё не изменяет исходный код и происходит во время компиляции, в то время как макросы изменяют исходный код перед компиляцией.
Нужно иметь ввиду, что многие современные компиляторы могут игнорировать такие "подсказки".
Встраивание это важная оптимизация, однако оказываемый эффект на производительность может быть различным. Как правило, некоторое количество встраиваний помогает улучшить быстродействие при очень незначительных затратах пространства, но избыточность использования может замедлить программу из-за того, что встроенный код станет потреблять слишком много кэша инструкций, а также займёт значительное количество пространства.
Ключевое слово inline говорит линковщику, что эта функция будет определена в разных единицах трансляции, игнорировать это. Линковщик должен убедится, что все единицы трансляции используют один и тот же экземпляр.
Определение шаблонов с inlineбессмысленно, потому что они уже используют семантику линковки, аналогичную предоставляемой inline. Однако, для явной (explicit) специализации и инициализации шаблонов требуется использовать inline.
Когда следует использовать ключевое слово inline для функции или метода в C\C++?
Практически только в случаях когда вы хотите определить функцию в заголовочом файле. Точнее, когда более одного определения функции встречается в различных единицах трансляции. Будет хорошей идеей поместить маленькие (однострочные) функции в заголовок (файл .h), так как это даст больше информации для оптимизации компилятору. Однако, это увеличит время сборки.
Когда не следует использовать ключевое слово inline для функции или метода в C\C++?
Не используйте inline только ради ощущения, что ваш код заработает быстрее.
Когда компилятор не знает, что нужно сделать функцию встроенной?
В целом, компилятор умеет выполнять такую оптимизацию лучше вас. Однако, он не сможет это сделать, если нет определения функции (в данной единице трансляции). Как правило, в максимально оптимизированном коде все private методы встраиваются, просили вы того, или нет.
Для предотвращения встраивания в GCC используйте __attribute__(( noinline )), в Visual Studio __declspec(noinline).
Директивы слово static и extern
Директивы static и extern используются больше для организации структуры кода, а не для оптимизации, но знать их необходимо.- static — имя функции/переменной не может быть использовано в других единицах трансляции (модулях). Линковщик должен убедиться, что это случайно не сделано. Для глобальных переменных - ограничивает область видимости только одним файлом. Для локальных переменных (переменные внутри функций) директива static показывает, что переменная должна жить всё время работы программы (не на стеке). Переменная будет существовать (память под неё выделена) с момента запуска программы и до её завершения — независимо от того, в какой функции она объявлена и сколько раз эта функция вызывается/завершается.
- extern — указывает компилятору использовать это имя (переменной/функции) в данной единице трансляции (модуле), но не ругаться, если оно не определено. Линковщик разберётся и убедится, что у каждого символа есть адрес. При разборе конкретного модуля компилятор видит, что переменная/функция существует где-то ещё, память под неё уже выделена (или будет выделена компоновщиком), и не нужно создавать ещё одну копию. Без extern при повторном объявлении глобальной переменной в нескольких файлах произойдёт ошибка множественного определения (multiple definition) на этапе линковки.
Если совсем коротко — extern нужна, чтобы безопасно делить глобальные данные и функции между разными .c файлами.
Все три ключевых слова (inline, static и extern) используются в основном линковщиком, а не компилятором.
Хранение строк во flash (PSTR и аналоги в других МК)
В микроконтроллерах, где SRAM (оперативная память) ограничена, а flash (программная память) больше и используется для кода/констант, если не обращать внимания на то где хранятся строки, можно забить SRAM.По умолчанию строковые литералы (как "Test. Hello world!") размещаются в SRAM, даже если они константны. Это тратит ценную SRAM, которая нужна для переменных, стека и т.д. Например:
Serial.print("Test. Hello world!"); // Строка копируется в SRAM, занимает ~20 байт постоянно.
PSTR это инструкция используемая для оптимизации памяти в микроконтроллерах AVR, где SRAM (оперативная память) ограничена, а flash (программная память) больше и используется для кода/констант.
Чтобы хранить строку только во flash и экономить SRAM, используйте макрос F() (для простоты) или PSTR с функциями чтения из PROGMEM.
// С хелпером F()
Serial.print(F("Test. Hello world!")); // Строка остается во flash, читается по мере вывода, SRAM не тратится на хранение.
// Под капотом F() эквивалентно: reinterpret_cast(PSTR("Test. Hello world!")).
C директивой PSTR:
const char PROGMEM *str = PSTR("This text in flash");
// Читать побайтово: pgm_read_byte(&str[i]) или специализированными функциями вроде strcpy_P.
Плюсы: Экономит SRAM для большего количества строк или других данных.
Минусы: Доступ медленнее, нельзя модифицировать строки во flash. Работает только с константными строками.
Данная оптимизация релевантна для AVR-based MCU (Arduino Uno, Nano, Mega на ATmega), где SRAM мала (0.5–8 КБ), а flash 16–256 КБ.
Для других платформ:
- ESP8266/ESP32: Поддерживается для совместимости, но менее критично (Von Neumann-архитектура, унифицированная память, много RAM — до 320 КБ), хотя помогает если RAM заканчивается.
- ARM-based (SAMD в Zero, Due): Аналогично, поддержка есть, но эффект меньше из-за большего RAM.
- PIC тоже имеет подобную оптимизацию, код немного отличается для разных компиляторов.
В компиляторе XC8 (рекомендуемый для новых проектов):
Достаточно просто объявить строки как const char str[] = "Text"; — компилятор размещает их во flash автоматически для поддерживаемых устройств. Доступ: Для чтения используйте стандартные функции (компилятор генерирует код с tblrd/tblptr), но для вывода (например, UART) может потребоваться копирование в RAM с помощью strcpy() или специальных функций вроде strcpyram() из stdlib. Для printf-like используйте макросы или библиотеки, поддерживающие flash (например, printf() работает с const, но читает по байтам).
В компиляторе mikroC:
const code char str[] = "Text"; // размещает во flash.
Доступ: Функции вроде strcpy() работают напрямую, или Lcd_Out_CP(str) для вывода из flash. - Не релевантно для MCU без Harvard-архитектуры или с большим RAM (например, STM32 в Blue Pill).