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

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

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

, 👍62

Обсуждение

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

Я не знаю, *плохо ли это*, но я думаю, что это не используется, потому что у вас почти никогда не заканчивается оперативная память для большинства скетчей, и это просто пустая трата флэш-памяти и драгоценных тактов. Кроме того, не забывайте о области действия (хотя я не знаю, выделено ли это пространство для всех переменных)., @Anonymous Penguin

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


8 ответов


19

Обычно при написании скетчей Arduino вы избегаете динамического выделения памяти (будь то с malloc или new для экземпляров C++), люди предпочитают использовать global -или статические-переменные или локальные (стековые) переменные.

Использование динамического размещения может привести к нескольким проблемам:

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

,

9

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

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

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

,

51

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

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

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

Обновление от 16.02.23:

Мне любопытно, почему реализация malloc в библиотеке Arduino не реализовать некоторое объединение свободных блоков, как в полной ОС.

Об этом интересно подумать, но давайте сначала проясним, что в C/C++ функции malloc() и free() реализованы как библиотечные функции на уровне приложения, а не на уровне ОС, даже в основных операционных системах. И они основаны на фрагментации кучи, как это делают функции malloc() в Arduino.

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

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

Обновление от 18.02.23:

@edgarbonet отмечает, что free() Arduino действительно пытается объединить свободные блоки. Это хороший пример алгоритма, который работает в определенных случаях: при правильном выполнении он будет работать для "последний вышел / первый пришел"; случаев и в некоторой степени для некоторых других случаев.

Проблема в том, что большинство наших шаблонов использования не очень регулярны, если они вообще регулярны. Алгоритм, который не выполняет разбиение, а просто выделяет & освобождение фрагментов фиксированного размера (скорее, как одалживание библиотечных книг) может по-прежнему исчерпать память, но не приведет к фрагментации ресурсов. На самом деле, поскольку он не разделяется, ему потребуется больше памяти для обработки тех же запросов произвольного размера, чем даже объединению malloc() и free( ).

,

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

Мне любопытно, почему реализация malloc в библиотеке Arduino не реализует объединение свободных блоков, как в полной ОС., @vasilescur

@vasilescur: Реализация malloc()/free() AVR объединяет смежные свободные блоки., @Edgar Bonet


-2

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

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

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

,

Интересно, что вы использовали дрон в качестве примера. Согласно этой статье (http://mil-embedded.com/articles/justifiably-apis-militaryaerospace-embedded-code/), «Из-за риска динамическое выделение памяти запрещено в соответствии со стандартом DO-178B в целях безопасности. -критический код встроенной авионики»., @Gabriel Staples

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

Динамическое размещение кажется приглашением для вашей программы продемонстрировать ограничения вычислений, описанные в проблеме остановки. Есть некоторые среды, которые могут справиться с небольшим риском такой остановки, и существуют среды (космическая, оборонная, медицинская и т. д.), которые не допускают никакого контролируемого риска, поэтому они запрещают операции, которые «не должны» потерпеть неудачу, потому что «это должно работать» недостаточно, когда вы запускаете ракету или управляете аппаратом сердца/легких., @Kelly S. French


21

Я взглянул на алгоритм, используемый 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);
}

Если внутри do_whatever_with() нет malloc или если эта функция освобождает все, что он выделяет, то вы защищены от фрагментации.

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

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

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

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

,

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


10

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

Пример: у меня есть класс последовательного пакета (библиотека), который может принимать данные произвольной длины (может быть структура, массив uint16_t и т. д.). На стороне отправителя этого класса вы просто сообщаете методу Packet.send() адрес объекта, который вы хотите отправить, и порт HardwareSerial, через который вы хотите его отправить. Однако на принимающей стороне мне нужен динамически выделяемый приемный буфер для хранения этой входящей полезной нагрузки, поскольку эта полезная нагрузка может иметь другую структуру в любой момент, например, в зависимости от состояния приложения. ЕСЛИ я когда-либо отправляю только одну структуру туда и обратно, я бы просто сделал размер буфера таким, каким он должен быть во время компиляции. Но в случае, когда длина пакетов с течением времени может меняться, malloc() и free() не так уж плохи.

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

// можно найти по адресу 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); // Беглый взгляд
}

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

,

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

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

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

@ChrisStratton Именно из-за ограничений памяти я считаю, что динамическое выделение буфера только того размера, который требуется в данный момент, является идеальным. Если буфер не нужен во время обычной работы, память доступна для чего-то другого. Но я понимаю, что вы говорите: выделите зарезервированное пространство, чтобы что-то еще не отняло место, которое вам может понадобиться для буфера. Вся отличная информация и мысли., @StuffAndyMakes

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

@ChrisStratton Правильно, этот фрагмент кода не проверяет, работает ли malloc () или нет. Опять же, этот кусок кода использует общеизвестно толстые библиотеки и подпрограммы Arduino. :) Спасибо за ваше понимание. Очень разумно и ценно., @StuffAndyMakes

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


4

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

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

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

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

Схема памяти следующая:

Карта SRAM

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

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

,

Согласен с этим: «[H]ard встраиваемое программирование в реальном времени — это самый сложный навык программирования»., @StuffAndyMakes

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

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

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


-1

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

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

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

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

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

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

,

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

Я допускаю некоторое риторическое преувеличение, но на самом деле суть в том, что если вы хотите гарантировать поведение, использование кучи подразумевает гораздо более высокий уровень риска, чем использование только стека., @Kelly S. French