Использование malloc () и free () действительно плохая идея для Arduino?


49

Использование malloc()и free()кажется довольно редким в мире Arduino. Он используется в чистом AVR C гораздо чаще, но все же с осторожностью.

Является ли это действительно плохая идея использования malloc()и free()с Arduino?


2
в противном случае у вас очень быстро кончится память, и если вы знаете, сколько памяти вы будете использовать, вы все равно могли бы статически распределить ее
ratchet freak

1
Я не знаю, если это плохо , но я думаю, что это не используется, потому что вы почти никогда не исчерпываете ОЗУ для большинства набросков, а это просто трата флэш-памяти и драгоценных тактов. Кроме того, не забывайте о области действия (хотя я не знаю, выделено ли это пространство для всех переменных).
Анонимный Пингвин

4
Как обычно, правильный ответ - «это зависит». Вы не предоставили достаточно информации, чтобы точно знать, подходит ли вам динамическое размещение.
WineSoaked

Ответы:


40

Мое общее правило для встроенных систем - использовать только malloc()большие буферы и только один раз при запуске программы, например, в setup(). Проблема возникает, когда вы выделяете и освобождаете память. В течение продолжительного сеанса память становится фрагментированной, и в конечном итоге выделение не выполняется из-за отсутствия достаточно большой свободной области, даже если общая свободная память более чем достаточна для запроса.

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

Если мне нужно динамическое выделение памяти во встроенной системе, я обычно malloc()или предпочтительно статически выделяю большой пул и делю его на буферы фиксированного размера (или один пул каждый из малых и больших буферов соответственно) и делаю свое собственное распределение / удаление из этого пула. Затем каждый запрос на любой объем памяти вплоть до фиксированного размера буфера удовлетворяется одним из этих буферов. Вызывающей функции не нужно знать, больше ли она, чем запрошено, и, избегая разделения и повторного объединения блоков, мы решаем фрагментацию. Конечно, утечки памяти все еще могут происходить, если в программе есть ошибки выделения / удаления.


Еще одно историческое замечание: это быстро привело к сегменту BSS, который позволил программе обнулить собственную память для инициализации, не копируя нули во время загрузки программы.
rsaxvc

16

Как правило, при написании набросков 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удалении экземпляра.


14

Я взглянул на алгоритм, используемый в malloc()avr-libc, и, похоже, есть несколько шаблонов использования, которые безопасны с точки зрения фрагментации кучи:

1. Выделяйте только долгоживущие буферы

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

2. Выделите только недолговечные буферы

Значение: вы освобождаете буфер перед выделением чего-либо еще. Разумный пример может выглядеть так:

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()или если эта функция освобождает все, что она выделяет, то вы защищены от фрагментации.

3. Всегда освобождайте последний выделенный буфер

Это обобщение двух предыдущих случаев. Если вы используете кучу как стек (последний пришел первым, то вышел), он будет вести себя как стек, а не как фрагмент. Следует отметить, что в этом случае безопасно изменить размер последнего выделенного буфера realloc().

4. Всегда выделяйте одинаковый размер

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


1
Следует избегать шаблона 2, так как он добавляет циклы для malloc () и free (), когда это можно сделать с помощью "char buffer [size];" (в C ++). Я также хотел бы добавить анти-паттерн "Никогда от ISR".
Микаэль Патель

9

Использование динамического распределения (через malloc/ freeили new/ delete) само по себе не плохо как таковое. Фактически, для чего-то вроде обработки строк (например, через Stringобъект), это часто весьма полезно. Это связано с тем, что во многих эскизах используются несколько небольших фрагментов строк, которые в конечном итоге объединяются в более крупный. Использование динамического выделения позволяет вам использовать столько памяти, сколько вам нужно для каждого. Напротив, использование статического буфера фиксированного размера для каждого из них может привести к потере много места (что приводит к нехватке памяти гораздо быстрее), хотя это полностью зависит от контекста.

Учитывая все это, очень важно убедиться, что использование памяти предсказуемо. Разрешение скетчу использовать произвольные объемы памяти в зависимости от обстоятельств времени выполнения (например, ввода) может рано или поздно вызвать проблему. В некоторых случаях это может быть совершенно безопасно, например, если вы знаете, что использование никогда не будет составлять много. Эскизы могут меняться в процессе программирования. Предположение, сделанное ранее, может быть забыто, когда что-то будет изменено позже, что приведет к непредвиденной проблеме.

Для надежности обычно лучше работать с буферами фиксированного размера, где это возможно, и спроектировать эскиз так, чтобы он работал с этими ограничениями с самого начала. Это означает, что любые будущие изменения в эскизе или любые неожиданные обстоятельства во время выполнения, мы надеемся, не вызовут никаких проблем с памятью.


6

Я не согласен с людьми, которые думают, что вы не должны использовать это или вообще не нужно. Я считаю, что это может быть опасно, если вы не знаете все подробности, но это полезно. У меня есть случаи, когда я не знаю (и не должен знать) размер структуры или буфера (во время компиляции или во время выполнения), особенно когда речь идет о библиотеках, которые я посылаю в мир. Я согласен, что если ваше приложение имеет дело только с одной, известной структурой, вы должны просто выпекать с таким размером во время компиляции.

Пример: у меня есть класс последовательного пакета (библиотека), который может принимать полезные данные произвольной длины (может быть 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.


2
Ваш тестовый код соответствует шаблону использования 2. Выделите только недолговечные буферы, которые я описал в моем предыдущем ответе. Это один из тех немногих шаблонов использования, которые известны как безопасные.
Эдгар Бонет

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

@EdgarBonet Да, именно так. Просто хотел поделиться.
StuffAndyMakes

1
Динамическое выделение буфера только необходимого размера рискованно, как если бы что-то еще выделялось до того, как вы его освободите, вы можете остаться с фрагментацией - памятью, которую вы не сможете использовать повторно. Кроме того, динамическое распределение имеет накладные расходы на отслеживание. Фиксированное распределение не означает, что вы не можете многократно использовать память, это просто означает, что вы должны работать над разделением в дизайне вашей программы. Для буфера с чисто локальной областью вы также можете взвесить использование стека. Вы также не проверили на возможность сбоя malloc ().
Крис Страттон

1
«это может быть опасно, если вы не знаете все подробности, но это полезно». в значительной степени подводит итог всей разработки на C / C ++. :-)
ThatAintWorking

4

Действительно ли плохая идея использовать malloc () и free () с Arduino?

Короткий ответ: да. Ниже приведены причины, по которым:

Все дело в понимании того, что такое MPU и как программировать в рамках ограничений доступных ресурсов. Arduino Uno использует ATmega328p MPU с флэш-памятью ISP 32 КБ, EEPROM 1024 Б и SRAM 2 КБ. Это не много ресурсов памяти.

Помните, что SRAM 2 КБ используется для всех глобальных переменных, строковых литералов, стека и возможного использования кучи. У стека также должна быть свободная комната для ISR.

Расположение памяти :

SRAM карта

Сегодняшние ПК / ноутбуки имеют объем памяти более чем в 1 000 000 раз. 1 Мбайт стекового пространства по умолчанию на поток не является чем-то необычным, но совершенно нереальным для MPU.

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


Аминь тому: «Встроенное программирование в реальном времени является самым сложным навыком программирования для овладения».
StuffAndyMakes

Время выполнения malloc всегда одинаково? Я могу себе представить, что malloc отнимает больше времени, пока он ищет в доступном ОЗУ слот, который подходит? Это было бы еще одним аргументом (помимо исчерпания памяти), чтобы не выделять память на ходу?
Пол

@Paul Алгоритмы кучи (malloc и free) обычно не имеют постоянного времени выполнения и не реентерабельны. Алгоритм содержит структуры поиска и данных, которые требуют блокировок при использовании потоков (параллелизм).
Микаэль Патель

0

Хорошо, я знаю, что это старый вопрос, но чем больше я читаю ответы, тем больше я возвращаюсь к наблюдению, которое кажется очевидным.

Проблема остановки реальна

Кажется, здесь есть связь с проблемой остановки Тьюринга. Разрешение динамического распределения увеличивает шансы на упомянутое «прекращение», поэтому вопрос становится вопросом толерантности к риску. Хотя удобно отмахиваться от вероятности malloc()неудачи и т. Д., Это все же верный результат. Вопрос, который задает ОП, похоже, касается техники, и да, важны детали используемых библиотек или конкретного MPU; разговор переходит к снижению риска остановки программы или любого другого ненормального завершения. Мы должны признать существование сред, которые терпят риск совершенно по-разному. Мой хобби-проект по отображению красивых цветов на светодиодной полосе не убьет кого-то, если случится что-то необычное, но MCU в аппарате искусственного кровообращения, скорее всего, убьет.

Здравствуйте, мистер Тьюринг, меня зовут гордыня

Что касается моей светодиодной ленты, мне все равно, если она заблокируется, я просто перезагрузить ее. Если бы я был на машине , сердце-легкой , контролируемый MCU последствия него блокировку или не работать буквально жизнь и смерть, поэтому вопрос о том, malloc()и free()должно быть разделение между тем, как предполагаемыми программными сделками с возможностью демонстрации г - Знаменитая проблема Тьюринга. Может быть легко забыть, что это математическое доказательство, и убедить себя в том, что, если мы достаточно умны, мы можем избежать потери вычислительных возможностей.

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


Я не думаю, что проблема Остановки применима в этой конкретной ситуации, учитывая тот факт, что использование кучи не обязательно произвольно. Если используется четко определенным образом, использование кучи становится предсказуемо «безопасным». Смысл проблемы Холтинга состоял в том, чтобы выяснить, можно ли определить, что происходит с обязательно произвольным и не очень хорошо определенным алгоритмом. Это действительно гораздо больше относится к программированию в более широком смысле, и, как таковое, я считаю, что оно здесь не особо актуально. Я даже не считаю уместным быть полностью честным.
Джонатан Грей

Я допускаю некоторое риторическое преувеличение, но суть в том, что если вы хотите гарантировать поведение, использование кучи подразумевает уровень риска, который намного выше, чем использование только стека.
Келли С. Френч

-3

Нет, но они должны использоваться очень осторожно в отношении освобождения () выделенной памяти. Я никогда не понимал, почему люди говорят, что прямого управления памятью следует избегать, поскольку оно подразумевает уровень некомпетентности, который обычно несовместим с разработкой программного обеспечения.

Допустим, вы используете Arduino для управления дроном. Любая ошибка в любой части вашего кода может привести к тому, что он упадет с неба и нанесет вред кому-либо или чему-либо. Другими словами, если кому-то не хватает компетенции использовать malloc, он, вероятно, вообще не должен кодировать, так как есть много других областей, где небольшие ошибки могут вызвать серьезные проблемы.

Труднее ли выследить и исправить ошибки, вызванные malloc? Да, но это скорее вопрос разочарования, а не риска. Что касается риска, любая часть вашего кода может быть такой же или более рискованной, чем malloc, если вы не предпримете шаги, чтобы убедиться, что все сделано правильно.


4
Интересно, что вы использовали дрон в качестве примера. Согласно этой статье ( mil-embedded.com/articles/… ), «из-за риска динамическое выделение памяти запрещено в соответствии со стандартом DO-178B в коде встроенной авионики, критичной к безопасности».
Габриэль Стейплс

DARPA имеет давнюю историю, позволяющую подрядчикам разрабатывать спецификации, которые соответствуют их собственной платформе - почему бы им это не делать, когда счет платят налоги. Вот почему им стоит 10 миллиардов долларов, чтобы развить то, что другие могут сделать с 10 000 долларов. Звучит так, как будто вы используете военно-промышленный комплекс в качестве честной ссылки.
JSON

Динамическое распределение похоже на приглашение вашей программы продемонстрировать пределы вычислений, описанные в проблеме остановки. Существуют некоторые среды, которые могут справиться с небольшим уровнем риска такой остановки, и существуют среды (космическая, оборонная, медицинская и т. Д.), Которые не допустят какого-либо контролируемого риска, поэтому они запрещают операции, которые «не должны» потерпеть неудачу, потому что «это должно работать» не достаточно хорошо, когда вы запускаете ракету или управляете машиной «сердце / легкие».
Келли С. Френч
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.