Использование malloc()
и free()
кажется довольно редким в мире Arduino. Он используется в чистом AVR C гораздо чаще, но все же с осторожностью.
Является ли это действительно плохая идея использования malloc()
и free()
с Arduino?
Использование malloc()
и free()
кажется довольно редким в мире Arduino. Он используется в чистом AVR C гораздо чаще, но все же с осторожностью.
Является ли это действительно плохая идея использования malloc()
и free()
с Arduino?
Ответы:
Мое общее правило для встроенных систем - использовать только malloc()
большие буферы и только один раз при запуске программы, например, в setup()
. Проблема возникает, когда вы выделяете и освобождаете память. В течение продолжительного сеанса память становится фрагментированной, и в конечном итоге выделение не выполняется из-за отсутствия достаточно большой свободной области, даже если общая свободная память более чем достаточна для запроса.
(Историческая перспектива, пропустите, если не интересно): В зависимости от реализации загрузчика, единственным преимуществом выделения во время выполнения по сравнению с распределением во время компиляции (инициализированные глобальные переменные) является размер шестнадцатеричного файла. Когда встроенные системы создавались с готовых компьютеров, имеющих всю энергозависимую память, программа часто загружалась во встроенную систему из сети или с измерительного компьютера, и время загрузки иногда было проблемой. Исключение из изображения буферов, заполненных нулями, может значительно сократить время.)
Если мне нужно динамическое выделение памяти во встроенной системе, я обычно malloc()
или предпочтительно статически выделяю большой пул и делю его на буферы фиксированного размера (или один пул каждый из малых и больших буферов соответственно) и делаю свое собственное распределение / удаление из этого пула. Затем каждый запрос на любой объем памяти вплоть до фиксированного размера буфера удовлетворяется одним из этих буферов. Вызывающей функции не нужно знать, больше ли она, чем запрошено, и, избегая разделения и повторного объединения блоков, мы решаем фрагментацию. Конечно, утечки памяти все еще могут происходить, если в программе есть ошибки выделения / удаления.
Как правило, при написании набросков Arduino вы избегаете динамического размещения (будь то с экземплярами C ++ malloc
или new
для них), люди предпочитают использовать глобальные static
переменные или локальные (стековые) переменные.
Использование динамического выделения может привести к нескольким проблемам:
malloc
/ free
), когда куча становится больше, чем фактический объем памяти, выделенный в данный моментВ большинстве ситуаций, с которыми я сталкивался, динамическое выделение либо не было необходимым, либо его можно было избежать с помощью макросов, как в следующем примере кода:
MySketch.ino
#define BUFFER_SIZE 32
#include "Dummy.h"
Dummy.h
class Dummy
{
byte buffer[BUFFER_SIZE];
...
};
Без #define BUFFER_SIZE
, если бы мы хотели, чтобы Dummy
класс имел нефиксированный buffer
размер, нам пришлось бы использовать динамическое распределение следующим образом:
class Dummy
{
const byte* buffer;
public:
Dummy(int size):buffer(new byte[size])
{
}
~Dummy()
{
delete [] bufer;
}
};
В этом случае у нас больше параметров, чем в первом примере (например, мы используем разные Dummy
объекты с разным buffer
размером для каждого), но у нас могут возникнуть проблемы с фрагментацией кучи.
Обратите внимание на использование деструктора, чтобы гарантировать, что динамически выделенная память buffer
будет освобождена при Dummy
удалении экземпляра.
Я взглянул на алгоритм, используемый в malloc()
avr-libc, и, похоже, есть несколько шаблонов использования, которые безопасны с точки зрения фрагментации кучи:
Под этим я подразумеваю: выделяйте все, что вам нужно в начале программы, и никогда не освобождайте ее. Конечно, в этом случае вы также можете использовать статические буферы ...
Значение: вы освобождаете буфер перед выделением чего-либо еще. Разумный пример может выглядеть так:
void foo()
{
size_t size = figure_out_needs();
char * buffer = malloc(size);
if (!buffer) fail();
do_whatever_with(buffer);
free(buffer);
}
Если внутри нет malloc do_whatever_with()
или если эта функция освобождает все, что она выделяет, то вы защищены от фрагментации.
Это обобщение двух предыдущих случаев. Если вы используете кучу как стек (последний пришел первым, то вышел), он будет вести себя как стек, а не как фрагмент. Следует отметить, что в этом случае безопасно изменить размер последнего выделенного буфера realloc()
.
Это не предотвратит фрагментацию, но это безопасно в том смысле, что куча не будет расти больше, чем максимально используемый размер. Если все ваши буферы имеют одинаковый размер, вы можете быть уверены, что всякий раз, когда вы освобождаете один из них, слот будет доступен для последующих распределений.
Использование динамического распределения (через malloc
/ free
или new
/ delete
) само по себе не плохо как таковое. Фактически, для чего-то вроде обработки строк (например, через String
объект), это часто весьма полезно. Это связано с тем, что во многих эскизах используются несколько небольших фрагментов строк, которые в конечном итоге объединяются в более крупный. Использование динамического выделения позволяет вам использовать столько памяти, сколько вам нужно для каждого. Напротив, использование статического буфера фиксированного размера для каждого из них может привести к потере много места (что приводит к нехватке памяти гораздо быстрее), хотя это полностью зависит от контекста.
Учитывая все это, очень важно убедиться, что использование памяти предсказуемо. Разрешение скетчу использовать произвольные объемы памяти в зависимости от обстоятельств времени выполнения (например, ввода) может рано или поздно вызвать проблему. В некоторых случаях это может быть совершенно безопасно, например, если вы знаете, что использование никогда не будет составлять много. Эскизы могут меняться в процессе программирования. Предположение, сделанное ранее, может быть забыто, когда что-то будет изменено позже, что приведет к непредвиденной проблеме.
Для надежности обычно лучше работать с буферами фиксированного размера, где это возможно, и спроектировать эскиз так, чтобы он работал с этими ограничениями с самого начала. Это означает, что любые будущие изменения в эскизе или любые неожиданные обстоятельства во время выполнения, мы надеемся, не вызовут никаких проблем с памятью.
Я не согласен с людьми, которые думают, что вы не должны использовать это или вообще не нужно. Я считаю, что это может быть опасно, если вы не знаете все подробности, но это полезно. У меня есть случаи, когда я не знаю (и не должен знать) размер структуры или буфера (во время компиляции или во время выполнения), особенно когда речь идет о библиотеках, которые я посылаю в мир. Я согласен, что если ваше приложение имеет дело только с одной, известной структурой, вы должны просто выпекать с таким размером во время компиляции.
Пример: у меня есть класс последовательного пакета (библиотека), который может принимать полезные данные произвольной длины (может быть struct, массив uint16_t и т. Д.). В конце отправки этого класса вы просто сообщаете методу Packet.send () адрес того, что вы хотите отправить, и порт HardwareSerial, через который вы хотите его отправить. Однако на принимающей стороне мне нужен динамически распределяемый приемный буфер для хранения этой входящей полезной нагрузки, так как эта полезная нагрузка может быть другой структурой в любой данный момент, например, в зависимости от состояния приложения. Если я отправляю только одну структуру туда и обратно, я бы просто сделал буфер таким, каким он должен быть во время компиляции. Но в случае, когда пакеты со временем могут быть разной длины, malloc () и free () не так уж плохи.
Я провел тесты со следующим кодом в течение нескольких дней, позволяя ему непрерывно работать в цикле, и я не нашел никаких доказательств фрагментации памяти. После освобождения динамически распределяемой памяти свободное количество возвращается к своему предыдущему значению.
// found at learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
uint8_t *_tester;
while(1) {
uint8_t len = random(1, 1000);
Serial.println("-------------------------------------");
Serial.println("len is " + String(len, DEC));
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("alloating _tester memory");
_tester = (uint8_t *)malloc(len);
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("Filling _tester");
for (uint8_t i = 0; i < len; i++) {
_tester[i] = 255;
}
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("freeing _tester memory");
free(_tester); _tester = NULL;
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
delay(1000); // quick look
}
Я не видел какой-либо деградации в оперативной памяти или в моей способности динамически распределять ее с помощью этого метода, поэтому я бы сказал, что это жизнеспособный инструмент. FWIW.
Действительно ли плохая идея использовать malloc () и free () с Arduino?
Короткий ответ: да. Ниже приведены причины, по которым:
Все дело в понимании того, что такое MPU и как программировать в рамках ограничений доступных ресурсов. Arduino Uno использует ATmega328p MPU с флэш-памятью ISP 32 КБ, EEPROM 1024 Б и SRAM 2 КБ. Это не много ресурсов памяти.
Помните, что SRAM 2 КБ используется для всех глобальных переменных, строковых литералов, стека и возможного использования кучи. У стека также должна быть свободная комната для ISR.
Расположение памяти :
Сегодняшние ПК / ноутбуки имеют объем памяти более чем в 1 000 000 раз. 1 Мбайт стекового пространства по умолчанию на поток не является чем-то необычным, но совершенно нереальным для MPU.
Внедренный программный проект должен делать ресурсный бюджет. Это оценка задержки ISR, необходимого пространства памяти, вычислительной мощности, циклов инструкций и т. Д. К сожалению, бесплатных обедов не существует, а сложное программирование в режиме реального времени является самым сложным навыком программирования для освоения.
Хорошо, я знаю, что это старый вопрос, но чем больше я читаю ответы, тем больше я возвращаюсь к наблюдению, которое кажется очевидным.
Кажется, здесь есть связь с проблемой остановки Тьюринга. Разрешение динамического распределения увеличивает шансы на упомянутое «прекращение», поэтому вопрос становится вопросом толерантности к риску. Хотя удобно отмахиваться от вероятности malloc()
неудачи и т. Д., Это все же верный результат. Вопрос, который задает ОП, похоже, касается техники, и да, важны детали используемых библиотек или конкретного MPU; разговор переходит к снижению риска остановки программы или любого другого ненормального завершения. Мы должны признать существование сред, которые терпят риск совершенно по-разному. Мой хобби-проект по отображению красивых цветов на светодиодной полосе не убьет кого-то, если случится что-то необычное, но MCU в аппарате искусственного кровообращения, скорее всего, убьет.
Что касается моей светодиодной ленты, мне все равно, если она заблокируется, я просто перезагрузить ее. Если бы я был на машине , сердце-легкой , контролируемый MCU последствия него блокировку или не работать буквально жизнь и смерть, поэтому вопрос о том, malloc()
и free()
должно быть разделение между тем, как предполагаемыми программными сделками с возможностью демонстрации г - Знаменитая проблема Тьюринга. Может быть легко забыть, что это математическое доказательство, и убедить себя в том, что, если мы достаточно умны, мы можем избежать потери вычислительных возможностей.
На этот вопрос должно быть принято два ответа: один для тех, кто вынужден моргать, глядя перед лицом проблемы остановки, и один для всех остальных. В то время как большинство применений arduino, вероятно, не являются критически важными или приложениями для жизни и смерти, различие все еще существует независимо от того, какой MPU вы можете кодировать.
Нет, но они должны использоваться очень осторожно в отношении освобождения () выделенной памяти. Я никогда не понимал, почему люди говорят, что прямого управления памятью следует избегать, поскольку оно подразумевает уровень некомпетентности, который обычно несовместим с разработкой программного обеспечения.
Допустим, вы используете Arduino для управления дроном. Любая ошибка в любой части вашего кода может привести к тому, что он упадет с неба и нанесет вред кому-либо или чему-либо. Другими словами, если кому-то не хватает компетенции использовать malloc, он, вероятно, вообще не должен кодировать, так как есть много других областей, где небольшие ошибки могут вызвать серьезные проблемы.
Труднее ли выследить и исправить ошибки, вызванные malloc? Да, но это скорее вопрос разочарования, а не риска. Что касается риска, любая часть вашего кода может быть такой же или более рискованной, чем malloc, если вы не предпримете шаги, чтобы убедиться, что все сделано правильно.