Детальный анализ использования памяти

При компиляции скетча после связывания сборка выводит своего рода прогноз использования оперативной памяти, например:

Минимальное использование памяти: 1456 байт (71% от максимального значения 2048 байт)

Можно ли каким-то образом получить разбивку или детализацию этих 1456 байтов и подробную информацию о том, где именно в моем коде или во включенных библиотеках они используются?

, 👍7


4 ответа


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

10

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

avr-nm -Crtd --size-sort the_program.elf | grep -i ' [dbv] '

avr-nm-это утилита для отображения таблицы символов. Обычно он поставляется с Arduino IDE. Он вызывается здесь со следующими опциями:

  • --size-sort довольно явный
  • -C означает “разобрать имена C++”.
  • -r означает “обратная сортировка”, то есть сортировка от наибольшего к наименьшему
  • -td означает “отображать числа в десятичном, а не шестнадцатеричном формате”.

Это выведет список всех символов, имеющих размер, в три столбца: размер, тип и имя. Вас интересуют только символы , потребляющие оперативную память, то есть символы типа b (BSS или неинициализированные данные), d (инициализированные данные) и v (vtable), написанные в верхнем или нижнем регистре. Команда grep-это стандартная утилита Unix, которая используется здесь для извлечения только соответствующих строк из вывода avr-nm.

Пример вывода для небольшой программы:

00000068 B tx_buffer
00000068 B rx_buffer
00000034 B Serial
00000016 V vtable for HardwareSerial
00000004 B timer0_overflow_count
00000004 B timer0_millis
00000002 b loop::last_print
00000001 b timer0_fract

Первые 4 записи поступают из последовательного объекта. 3 записи , начинающиеся с timer0_, используются функциями хронометража Arduino (millis(), micros() и delay()). Запись с именем loop::last_print-это статическая локальная переменная, которую я объявил в loop().

,

Это выглядит многообещающе, но мне придется подождать до сегодняшнего вечера, чтобы попробовать. Будет ли он перечислять только "локальное" имя переменных? Я полагаю, что, например, переменные tx_buffer и rx_buffer, перечисленные в вашем примере, на самом деле являются HardwareSerial::_tx_buffer и HardwareSerial::_rx_buffer? Может ли опущение опции-C дать какой-либо намек на имя класса переменной?, @jarnbjo

@jarnbjo: Вы получаете полное имя переменной, например loop::last_print - это локальная переменная из loop() с именем last_print. Последовательные буферы на самом деле находятся вне "HardwareSerial" в моей версии ядра Arduino, а " HardwareSerial не имеет статических переменных, поэтому вы не видите HardwareSerial::foo. Переменные для каждого экземпляра являются частью единственного экземпляра Serial и не перечислены отдельно. Пропуск -C только повредит читабельности, например, он напечатает _ZZ4loopE10last_print вместо loop::last_print`., @Edgar Bonet

Понимаю. Поля в классе HardwareSerial были самым близким совпадением, которое я мог найти для tx_buffer и rx_buffer в текущей версии исходного кода библиотеки, и я не учел, что у вас может быть более старая версия библиотеки. В этом есть смысл., @jarnbjo

Это решило мою проблему. Я ожидал, что оптимизатор компилятора будет намного умнее, чем он есть на самом деле, и у меня было много зависимостей, связанных с двоичным файлом, которые никогда не используются. Не намного больше, чем удаление неиспользуемых библиотечных файлов, сэкономило мне почти 500 байт оперативной памяти., @jarnbjo


2

Просмотр списка сборок вашего кода-самый прямой способ узнать подробности распределения пространства. В моей системе Ubuntu Linux я использую сценарий оболочки (то есть файл, содержащий команды оболочки), как показано ниже, для создания списка сборок самого недавно скомпилированного файла .ino, который должен находиться в рабочем каталоге при выполнении сценария:

#!/bin/sh
# Look for .ino in current directory, and find recent /tmp/build...
item=*.ino
[ -z "$item" ] && echo ino file not found && exit
BDIR=/tmp/$(ls -t /tmp | egrep -m1 build.*tmp)
BASE=$(basename $item .ino)
avr-objdump -S -I$PWD $BDIR/$BASE.cpp.elf > $BASE.ino.asm

Все, что находится перед строкой avr-objdump, позволяет скрипту узнать имя текущего файла .ino и имя подкаталога /tmp/, в котором находится файл .ino.elf.

Вы также можете использовать avr-objdump, чтобы получить более краткий список размеров, как показано ниже, где $BDIR и $BASE, как в приведенном выше скрипте, представляют каталог сборки в /tmp/и базовое имя файла .ino.

avr-objdump -C -d $BDIR/$BASE.cpp.o| egrep -C2 '^Disassembly' | egrep -v '^--|^$|^Disassembly' | less

Это приведет к созданию списка с двухстрочными записями, как показано ниже, в котором каждая строка, начинающаяся с 00000000, является именем подпрограммы, а следующая строка показывает последнюю строку ее скомпилированного кода. В этом примере процедура setup_PowerDown() имеет 0xA4 байта кода, ISR __vector_13 имеет 0xC6 байт, keepAlive() имеет 0x10 байт и т. Д.

00000000 <setup_PowerDown()>:
  a4:   08 95           ret
00000000 <__vector_13>:
  c6:   18 95           reti
00000000 <keepAlive()>:
  10:   08 95           ret
00000000 <eeWrite1(unsigned char, unsigned char)>:
  3a:   08 95           ret
00000000 <eeWrite2(unsigned char, unsigned char)>:
  18:   0c 94 00 00     jmp     0       ; 0x0 <eeWrite2(unsigned char, unsigned char)>
00000000 <parValidate(unsigned char, unsigned char, unsigned char)>:
  64:   08 95           ret
00000000 <loadSettings()>:
  e0:   08 95           ret
00000000 <applySettings()>:
  84:   08 95           ret

Некоторые IDE предоставляют информацию о таблице символов, которая включает размеры подпрограмм, кадров стека и данных. Я не знаю, доступно ли это через Arduino IDE, AVR-libc или GNU Binutils. Таблица символов, предоставленная avr-readelf-s (как в следующей команде), предоставляет довольно подробную информацию о символах, которая кажется совершенно бесполезной в своем необработанном виде.

avr-readelf  -s  $BDIR/$BASE.cpp.o

Методы, описанные выше, являются несколько специальными и зависят от файлов, оставшихся после запуска avr-gcc или avr-g++. Я не знаю никаких “официальных” методов получения информации, которую вы хотите увидеть.

,

2

Вы можете автоматически генерировать файл "карты" каждый раз при компиляции. Это дает вам полную разбивку того, как все связано компоновщиком, включая размер каждого раздела и другую полезную отладочную информацию.

Простая модификация для platforms.txt в аппаратном обеспечении/arduino/avr-это все, что нужно. Найдите строку, начинающуюся compiler.c.elf.flags= и добавьте в ее конец:

 -Wl,-Map,"{build.path}/{build.project_name}.map"

Если хотите, вы можете заменить {build.path} на жестко закодированный путь, например на рабочий стол, а файл .map-на файл .txt, чтобы его можно было легко открыть блокнотом или чем бы вы ни пользовались (при условии, конечно, Windows).

,

Также очень полезно, но знаете ли вы, почему инструмент avr-nm, предложенный Эдгаром в его ответе, перечисляет использование памяти, которое не учитывается в этой сгенерированной карте. avr-nm, например, перечисляет 64 байта, используемые "rxBuffer" (полное имя "_ZL8rxBuffer"), чего нет указан на карте и который я не могу найти ни в одной зависимости., @jarnbjo


1

Это был отличный вопрос, и он помог мне недавно, так как я создаю довольно обширное приложение Arduino, используя Nano (ATmega328 и 32K программной памяти). В какой-то момент у меня почти закончилась память программы, и я был немного шокирован.
Я не думал, что код настолько велик.

Мои тесты и результаты

С комментариями, которые я прочитал в сообщениях об этом ответе, я решил создать пустое приложение и изучить результаты по мере добавления библиотек и т.д. Вот мои результаты (с использованием Arduino IDE 1.8.13):

  1. пустой скетч занимает 444 байта памяти программы (максимум 30720 байт).

  2. Sketch который включает в себя 6 библиотек✔ использует до 6218 байт

    <LiquidCrystal_I2C.h>, <Wire.h>,<ds3231.h>,<SD.h>,<EEPROM.h>,<SoftwareSerial.h>

  3. добавление одной строки Serial.begin(9600); использует 6324 байта ПРИМЕЧАНИЕ: 6324 - 6218 = 106 байт, хотя я добавил только 19 байт (символы для этой строки).

  4. Добавление еще одной строки Serial.print("a"); приводит к использованию 6342 байтов. Это еще 18 байтов (6324 - 6342 = 18), то есть точное количество символов, которые я добавил в эту строку (включая точку с запятой).

  5. Добавление 2-й строки с Serial.print("a"); приводит к 6358 (16 байт)

  6. Добавление 3-й строки Serial.print("a"); приводит к 6374 (16 дополнительным байтам).

Надеюсь, это немного поможет. Теперь я возвращаюсь к своему приложению и удаляю все последовательные строки, которые я использовал для вывода отладки. Теперь они просто съедают память программы. На самом деле, я, вероятно, даже буду использовать условную компиляцию (показанную в этом ответе SO), чтобы я все еще мог печатать при отладке.

Пустой скетч означает тот, в котором есть только структуры void setup() {} void loop() {} и ничего больше.

✔ Это библиотеки, которые я использую в своем проекте.

Я вернулся и добавил эти дополнительные строки с объемом используемой памяти программы, потому что был комментарий, что строки кода вообще не влияют на память. Конечно, они это делают. В какой-то момент вся память программы будет израсходована, и ваша программа больше не будет помещаться во флэш-память. Это то, с чем у меня была проблема, поскольку моя обширная программа (которая включает в себя датчики, ЖК-дисплей, sd-карту и Bluetooth) продолжает расти.

,

Длина строки источника *полностью* не связана с использованием памяти., @Dave Newton

@DaveNewton Да, я должен был подумать об этом, так как он должен подсчитывать скомпилированные байты, верно? может быть, это байты, добавленные в файл .elf?, @raddevus

Кроме того, я не уверен, что это *совершенно* не связано, потому что больше исходного кода означает большую программу и больший двоичный размер в конце концов. В противном случае зачем вам достигать предела по мере роста вашей программы и почему IDE постоянно предупреждает вас о размере вашего приложения?, @raddevus

Я сказал, что длина строки исходного кода не связана с использованием памяти-например, если открытый экземпляр последовательного драйвера был"ThisIsTheRealSerialDriverInstanceInUse.print ("a"); "скомпилированный размер был бы (при условии отсутствия отладочных символов) таким же, как" S. print ("а")`. Очевидно, что это более крупная программа... больше-но есть разница между размером исходного кода в байтах (удобочитаемая форма) и размером исполняемого файла., @Dave Newton

Другими словами, подсчет символов в строке источника, хотя и может совпадать с использованием памяти, но это просто совпадение., @Dave Newton

Спасибо за дополнительную информацию., @raddevus

Подробнее о комментарии Дэйва Ньютона: Короткая строка кода, вызывающая тяжеловесную библиотеку, может оказать огромное влияние на размер двоичного файла. Если вы добавляете переменные, которые хранят временные значения, которые компилятор должен оценить в любом случае, это приводит к росту вашего _source_, но имеет нулевой эффект на размер двоичного файла. Если вы добавляете локальные переменные в кэш "изменчивых" данных, ваш источник все равно растет, но двоичный файл на самом деле _shrinks_!, @Edgar Bonet

@EdgarBonet Спасибо за дополнительную информацию. Я просто пытаюсь выяснить, как "очистить" свой код до крайности, так как я добираюсь до пределов программного пространства. Я бы хотел, чтобы на этот счет было немного больше указаний, но это много проб и ошибок., @raddevus

Кроме того, когда я чистил свой код, я дошел до того, что он составлял 23436 байт prog mem. Я удалил все вызовы Serial.begin(), Serial.print() и т. Д., Перекомпилировал, и prog mem перешел на 23110, так что это будет ваша точка зрения. Но я не уверен, как удаление кода на самом деле увеличит использование памяти программы?? И да, я подтверждаю, что я только что удалил код. Очень странно., @raddevus

Просто удаление кода само по себе почти никогда не приведет к увеличению размера двоичного файла; я не думаю, что кто-то сказал, что это произойдет. Существует множество ресурсов для оптимизации общего кода по размеру (а также Arduino) и множество игр для компиляции/компоновки, в которые также можно играть., @Dave Newton

Мне очень жаль, что я все перепутал со своими цифрами, думая, что меньшее число означает меньше свободного места. Фух...я слишком долго на это смотрел. Удаление кода, похоже, помогает снизить число, но, конечно, не напрямую. Спасибо, что обсудили это со мной. , @raddevus

Удаление кода *непосредственно уменьшает двоичный размер; каждый раз, когда вы удаляете вызов, он уменьшает двоичный размер на количество байтов двоичного кода, необходимых для реализации вызова (инструкция вызова, перебор параметров и т. Д.). Как только начинаются игры компилятора/компоновщика, вы часто можете удалить неиспользуемый библиотечный код., @Dave Newton