ESP32-C3: оптимизация скорости работы |
Добавил(а) microsin |
Оптимизация скорости выполнения кода - ключевой элемент эффективности программного обеспечения. Код, который выполняется быстрее, дает другие положительные эффекты, наподобие снижение общего энергопотребления. Однако повышение скорости работы может быть сделано ценой других аспектов производительности, таких как минимизация объема кода [2] и расход оперативной памяти [3]. [Выбор: что нужно оптимизировать] Если функция в firmware приложения выполняется 1 раз в неделю, то скорее всего не имеет особого значения, сколько времени занимает её выполнение, 10 или 100 мс. Если же функция запускается постоянно с частотой 10 Гц, то становится очень важным, сколько времени она работает. Большинство кода firmware имеет очень небольшой набор функций, которые требуют оптимальной производительности. Возможно, эти функции выполняются очень частот, или должны удовлетворить некоторым особым требованиям приложения для минимизации латентности или повышение полосы пропускания. Действия по оптимизации должны быть нацелены именно на такие определенные функции. [Измерение производительности] Чтобы что-то улучшить в плане повышения скорости работы, необходимо какое-то средство, чтобы оценить текущую ситуацию. Если измерение производительности связано с взаимодействием с внешним миром, то вы можете измерить скорость работы напрямую. Например, можно применить код wifi/iperf и ethernet/iperf для измерения общей производительности сети, или можно использовать осциллограф или логический анализатор, чтобы измерить интервалы времени циклов работы периферийного устройства. В противном случае одним из способов измерения производительности является увеличение кода для выполнения измерений синхронизации: #include "esp_timer.h"
void measure_important_function(void) { const unsigned MEASUREMENTS = 5000; uint64_t start = esp_timer_get_time(); for (int retries = 0; retries < MEASUREMENTS; retries++) { important_function(); // Эта функция, скорость выполнения которой мы определяем } uint64_t end = esp_timer_get_time(); printf("%u iterations took %llu milliseconds (%llu microseconds per invocation)\n", MEASUREMENTS, (end - start)/1000, (end - start)/MEASUREMENTS); } Если для оценки времени выполнения не требуется очень большая точность, то для этого можно использовать вывод сообщений в лог. Макросы для вывода в лог, такие как ESP_LOGI [4], выводят в круглых скобках, в начале строки лога, значение абсолютного счетчика тиков RTOS (обычно длительность тика составляет 1 мс). Таким образом, можно напрямую оценить время выполнения участков кода с точностью до одного тика: I (579) cpu_start: Pro cpu start user code I (579) cpu_start: cpu freq: 160000000 I (579) cpu_start: Application information: I (579) cpu_start: Project name: spotter I (579) cpu_start: App version: 71ba7e4-dirty I (580) cpu_start: Compile time: Mar 22 2023 07:04:20 I (581) cpu_start: ELF file SHA256: 11272b759af54713... I (581) cpu_start: ESP-IDF: v4.4.1-dirty I (582) heap_init: Initializing. RAM available for dynamic allocation: I (583) heap_init: At 3FCACF50 len 000130B0 (76 KiB): DRAM I (584) heap_init: At 3FCC0000 len 0001F060 (124 KiB): STACK/DRAM I (586) spi_flash: detected chip: gd I (587) spi_flash: flash io: dio Выполнение целевого объекта несколько раз может помочь усреднить такие факторы, как переключатели контекста RTOS, накладные расходы на измерения и т.д. Вызов функции esp_timer_get_time() генерирует метки времени с точностью до микросекунды, однако этот способ измерения вносит небольшие ошибки, связанные с работой планировщика (переключением контекста) и вызовом функций синхронизации. Также можно использовать стандартные Unix-функции gettimeofday() и utime(), однако накладные расходы на их выполнение несколько больше. Иначе подключите hal/cpu_hal.h, и вызывайте HAL-функцию cpu_hal_get_cycle_count(), которая вернет текущее количество циклов CPU. У этой функции меньше накладные расходы по сравнению с другими функциями. Это хорошее средство для измерения коротких интервалов времени с высокой точностью. При измерении очень малых интервалов времени (которые много меньше 1 мс) производительность работы кэша flash иногда может дать большой разброс в результатах измерения времени, в зависимости от конкретного кода. Это происходит потому, что различные последовательности выполнения кода могут вызывать различные ситуации попадания или промахов кэша. Если тестируемый код большой, то эффект от накладных расходов доступа к flash обычно усредняется. Выполнение маленькой функции много раз может помочь снизить влияние промахов кэша. Чтобы полностью устранить накладные расходы на обращение к flash, поместите функцию в IRAM (см. далее "Целевые оптимизации"). Внешняя трассировка. Использование библиотеки трассировки и отладчика [5] позволяет проводить самые точные измерения с минимальным влиянием на тестируемый код. Производительность задач. Если разрешена опция CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS, то можно использовать API-функцию vTaskGetRunTimeStats(), чтобы получить runtime-информацию об относительном потреблении процессорного времени каждой задачей FreeRTOS. Утилита SEGGER SystemView является отличным инструментом для визуализации выполнения задачи и выявления проблем производительности для всей системы целиком. [Общее повышение скорости работы] Следующие оптимизации улучшат производительность практически любого кода, включая время загрузки, пропускная способность, задержки, и т. п.: · Установите опцию CONFIG_ESPTOOLPY_FLASHMODE в режим QIO или QOUT (Quad I/O). Оба этих режима почти в 2 раза повышают скорость, с который код загружается или выполняется из flash, в сравнении с режимом по умолчанию DIO. QIO немного быстрее QOUT, если оба этих режима поддерживаются. Обратите внимание, что для поддержки QIO или QOUT необходима как соответствующий тип микросхемы flash, так и электрические соединения между ESP32-C3 и микросхемой flash, иначе режимы quad I/O или SoC не будут корректно работать. Снижение накладных расходов лога. Несмотря на то, что стандартный вывод буферизируется, для приложения может существовать ограничение на частоту, с которой оно может печатать сообщения лога, как только буферы заполнятся. Это особенно важно во время первоначального запуска (startup), когда регистрируется множество событий, но такая ситуация может произойти также и в другое время. Есть несколько способов решить эту проблему: · Уменьшить объем вывода путем понижения значения опции CONFIG_LOG_DEFAULT_LEVEL (эквивалентно настройке загрузчика CONFIG_BOOTLOADER_LOG_LEVEL). Это также уменьшит размер двоичного кода, и уменьшит расход процессорного времени CPU, затраченного на обработку форматирования строк вывода. Не рекомендуется. Следующие опции также увеличат скорость выполнения, но их использование не рекомендуется, поскольку они снижают возможности по отладке, и могут увеличить степень серьезности любых ошибок. · Установите CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL в состояние "запрещено" (Compiler options → Assertion level → Disabled (sets -DNDEBUG)). Это также немного уменьшит размер двоичного кода firmware. Однако такая настройка может увеличить серьезность ошибок в firmware, включая баги, связанные с безопасностью. Если такая настройка необходима для оптимизации определенной функции, то рассмотрите вместо этого добавление #define NDEBUG в начало только одного файла исходного кода. [Целевые оптимизации] Следующие изменения повысят скорость работы выбранной части firmware приложения: · Поместите наиболее часто выполняемые участки кода во внутреннюю быструю оперативную память (IRAM). По умолчанию весь код приложения выполняется из кэша flash. Это означает, что могут быть ситуации "промаха кэша" для CPU, когда при загрузке следующей выполняемой инструкции он ждет синхронизации содержимого flash и кэша. Функции, которые копируются во время загрузки в IRAM, всегда будут выполняться на полной скорости без подобных задержек. IRAM это ограниченный ресурс, и при увеличении использования IRAM для кода это может уменьшить доступное количество DRAM, поэтому перемещение кода в IRAM требует стратегического планирования. Подробнее про IRAM (Instruction RAM) см. [6]. · Оптимизации таблицы переходов (jump table) могут быть заново разрешены для отдельных исходных файлов, которые не должны быть помещены в IRAM. Для часто используемых ветвей больших операторов switch/case это улучшит производительность. Как добавить опции компиляции -fjump-tables и -ftree-switch-conversion для отдельных исходных файлов, см. секцию "Управление компиляцией компонента" статьи [7]. [Уменьшение времени запуска] В дополнение к описанным выше общим улучшениям производительнсти следующие опции можно подстроить для значительного уменьшения времени запуска приложения (Startup Time): · Минимизация CONFIG_LOG_DEFAULT_LEVEL и CONFIG_BOOTLOADER_LOG_LEVEL значительно влияет на startup time. Чтобы вернуть более подробный лог после запуска приложения, установите CONFIG_LOG_MAXIMUM_LEVEL на минимальный уровень, и затем вызовите esp_log_level_set() для восстановления необходимого, повышенного уровня подробности вывода в лог. Функция main примера system/startup_time демонстрирует использование этого метода. Проект примера system/startup_time предварительно сконфигурирован для оптимизации startup time. Файл system/startup_time/sdkconfig.defaults содержит все эти настройки. Вы можете добавить их в конец файла sdkconfig своего проекта, однако сначала прочитайте документацию по каждой из настроек. [Приоритеты задач] Приложения ESP-IDF разрабатываются поверх FreeRTOS, и в приложении могут быть потоки задач, которым нужна повышенная пропускная способность по обработке и уменьшение задержек. Для таких задач нужно повысить приоритет, чтобы они запускались немедленно в нужные моменты времени. Приоритет задачи устанавливается при их создании, т. е. при вызове xTaskCreate() или xTaskCreatePinnedToCore(), и также можно поменять приоритет во время выполнения путем вызова vTaskPrioritySet(). Также необходимо обеспечить, чтобы высокоприоритетные задачи уступали процессорное время CPU остальным задачам (вызовом vTaskDelay(), sleep(), или путем блокировке на семафоре, очереди, оповещении задачи и т. п.), чтобы задачи с пониженным приоритетам также могли выполняться, и это не создавало проблем для всей системы. Task Watchdog Timer (TWDT) обеспечивает механизм автоматического определения "голода" задач по процессорному времени [8]. Однако имейте в виду, что таймаут Task WDT не всегда показывает проблему, иногда корректная работа firmware зависит от некоторых долго выполняющихся вычислений. В таких (редких) случаях может потребоваться подстройка времени таймаута Task WDT или даже его полный запрет. Приоритеты встроенных задач. ESP-IDF запускает несколько системных задач с фиксированными уровнями приоритета. Некоторые из них запускаются автоматически в процессе загрузки, другие запускаются только если firmware инициализирует некоторый определенный функционал. Для оптимизации производительности структурируйте приоритеты задач приложения таким образом, чтобы это не задерживало выполнение системных задач и не влияло на другие функции системы. Это может потребовать разделение определенной задачи. Например, организация выполнения операции, критичной по времени, в высокоприоритетной задаче или в обработчике прерывания, и перенос не критичных к времени частей кода в низкоприоритетную задачу. Заголовочный файл components/esp_system/include/esp_task.h содержит макросы для уровней прерывания, используемых для встроенных задач ESP-IDF. См. раздел Background Tasks документации [9] для дополнительной информации по системным задачам (system tasks). Для задач используются следующие общие приоритеты: · Главная задача системы (main task), которая запускает функцию app_main, имеет минимальный приоритет (1). Выбор приоритетов для задач приложения. В общем случае не рекомендуется устанавливать приоритет задачи выше, чем приоритет встроенных операций Wi-Fi/BT, поскольку отъем у них верени CPU может сделать систему нестабильной. Для очень коротких, критичных по времени выполнения операций, которые не используют сеть, используйте ISR или очень ограниченную задачу (очень короткими всплесками только runtime) с самым высоким приоритетом (24). Выбор приоритета 19 позволит низкоуровневому коду Wi-Fi/BT работать без задержек, вытесняя при этом код стека lwIP TCP/IP stack и другие не очень критичные к времени выполнения внутренние системные части кода - это самый лучший вариант выбора для критичных ко времени выполнения задач, которые не выполняют сетевых операций. Любая задача, которая выполняет сетевые операции TCP/IP, должна работать с приоритетом ниже, чем приоритет lwIP TCP/IP task (18), чтобы избежать проблем инверсии приоритета. Примечание: выполнение задач полностью приостанавливается, когда выполняется запись встроенного чипа SPI flash. Продолжат выполнение только обработчики прерываний, находящиеся во внутреннем ОЗУ (IRAM-Safe Interrupt Handlers). [Улучшение производительности прерываний] ESP-IDF поддерживает динамическое выделение прерываний с возможностью вытеснения. У каждого прерывания в системе есть приоритет, и прерывания с более высоким приоритетом вытесняют прерывания с более низким приоритетом. Обработчики прерывания выполняются, вытесняя любую активную задачу (кроме случая, когда выполнение задачи происходит внутри критической секции кода). По этой причине важно минимизировать время, которое CPU тратит на выполнение обработчика прерывания (ISR). Чтобы получить лучше быстродействие для определенного ISR: · Назначьте более важным прерываниями повышенный приоритет, используя при вызове esp_intr_alloc() такой флаг, как ESP_INTR_FLAG_LEVEL2 или ESP_INTR_FLAG_LEVEL3. [Повышение скорости работы сети] · Для Wi-Fi см. разделы "How to Improve Wi-Fi Performance" и "Wi-Fi Buffer Usage" статьи [13]. [Улучшение производительности ввода/вывода] Использование стандартных библиотечных функций C наподобие fread и fwrite вместо специфичных для платформы не буферизированных системных вызовов (unbuffered syscalls), таких как read и write, может быть медленным. Эти функции разработаны, чтобы быть портируемыми, так что они не обязательно оптимизированы для скорости, и могут вносить дополнительные накладные расходы на обработку и буферизацию. Специальная информация и советы для использования FatFS: · Максимальный размер запроса R/W == размеру кластера FatFS (единица выделяемой памяти). Примечание: установка повышенного размера буфера также увеличит использование кучи. [Ссылки] 1. ESP-IDF Maximizing Execution Speed site:espressif.com. |