Використовувати malloc () та free () дійсно погана ідея для Arduino?


49

Використання malloc()і free()здається досить рідкісним у світі Ардуїно. Він використовується в чистому AVR C набагато частіше, але все ж з обережністю.

Це дійсно погана ідея використовувати malloc()і free()з Arduino?


2
інакше у вас дуже швидко вичерпається пам'ять, і якщо ви знаєте, скільки пам’яті ви будете використовувати, ви все одно можете статично виділити її
ярост-карикатура

1
Я не знаю, чи це погано , але я думаю, що він не використовується, оскільки у вас майже ніколи не закінчується оперативна пам’ять для більшості ескізів, і це просто марно спалаху та дорогоцінних циклів годин. Крім того, не забувайте про область застосування (хоча я не знаю, чи все ще виділено цей простір для всіх змінних).
Анонімний пінгвін

4
Як завжди, правильна відповідь - "це залежить". Ви не надали достатньо інформації, щоб точно знати, чи підходить динамічний розподіл.
WineSoaked

Відповіді:


40

Моє загальне правило для вбудованих систем - це лише malloc()великі буфери і лише один раз, на початку програми, наприклад, в setup(). Проблема виникає, коли ви розподіляєте та розподіляєте пам'ять. Під час тривалого сеансу пам'ять стає фрагментарною, і з часом розподіл не вдається через відсутність достатньо великої вільної області, навіть якщо загальна вільна пам'ять є більш ніж достатньою для запиту.

(Історична перспектива, пропустити, якщо не цікавить): Залежно від реалізації завантажувача, єдиною перевагою розподілу часу виконання порівняно з розподілом часу компіляції (інтилізовані глобальні точки) є розмір шістнадцяткового файлу. Коли вбудовані системи були побудовані за допомогою нестандартних комп'ютерів, що мають всю енергонезалежну пам'ять, програма часто завантажувалася до вбудованої системи з мережі або приладового комп'ютера, і час завантаження іноді був проблемою. Якщо залишити буфери, наповнені нулями на зображенні, це може значно скоротити час.)

Якщо мені потрібен динамічний розподіл пам’яті у вбудованій системі, я, як правило malloc(), або бажано статично виділити великий пул і розділити його на буфери фіксованого розміру (або по одному пулу кожного з малих і великих буферів відповідно) і робити власне виділення / де-виділення з цього пулу. Тоді кожен запит на будь-який об'єм пам'яті до фіксованого розміру буфера задовольняється одним із цих буферів. Функція виклику не повинна знати, чи більша вона від запитуваної, і, уникаючи розщеплення та повторного поєднання блоків, ми вирішуємо фрагментацію. Звичайно, витоки пам’яті все ще можуть відбуватися, якщо програма виділяє / де-виділяє помилки.


Ще одна історична примітка, це швидко призвело до сегмента BSS, який дозволив програмі нулювати власну пам’ять для ініціалізації, не повільно копіюючи нулі під час завантаження програми.
rsaxvc

16

Як правило, при написанні Arduino ескізів, ви зможете уникнути динамічного розподілу (будь то з mallocабо newдля випадків , C ++), люди вважають за краще використовувати глобальні -OR static- змінні або локальні (стек) змінних.

Використання динамічного розподілу може призвести до кількох проблем:

  • пам'ять протікає (якщо ви втратите вказівник на раніше виділену пам'ять або, швидше за все, якщо ви забудете звільнити виділену пам'ять, коли вона вам більше не потрібна)
  • фрагментація купи (після декількох malloc/ freeдзвінків), де купа зростає більше, ніж фактичний обсяг пам'яті, виділений на даний момент

У більшості ситуацій, з якими я стикався, динамічне виділення або не було необхідним, або його можна було уникнути за допомогою макросів, як у наведеному нижче зразку коду:

MySketch.ino

#define BUFFER_SIZE 32
#include "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);
}

Якщо всередину немає молотка do_whatever_with(), або якщо ця функція звільняє все, що виділяється, ви захищені від фрагментації.

3. Завжди звільняйте останній виділений буфер

Це узагальнення двох попередніх випадків. Якщо ви використовуєте купу як стек (останній перший спочатку), він буде вести себе як стек, а не фрагмент. Слід зазначити, що в цьому випадку безпечно змінити розмір останнього виділеного буфера за допомогою realloc().

4. Завжди виділяйте однаковий розмір

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


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

9

Використання динамічного розподілу (через malloc/ freeабо new/ delete) само по собі не є поганим. Насправді, для чогось подібного обробку рядків (наприклад, через Stringоб'єкт), це часто досить корисно. Це тому, що багато ескізів використовують кілька невеликих фрагментів струн, які з часом об'єднуються у більші. Використання динамічного розподілу дозволяє використовувати лише стільки пам’яті, скільки потрібно для кожного. Навпаки, використання статичного буфера фіксованого розміру для кожного може призвести до того, що витрачається багато місця (змушує його вичерпати пам'ять набагато швидше), хоча це повністю залежить від контексту.

З урахуванням цього, дуже важливо переконатися, що використання пам'яті передбачувано. Дозволення ескізу використовувати довільну кількість пам'яті залежно від обставин виконання (наприклад, введення) може рано чи пізно спричинити проблему. У деяких випадках це може бути абсолютно безпечно, наприклад, якщо ви знаєте, що використання ніколи не зробить багато. Ескізи можуть змінюватися в процесі програмування. Припущення, зроблене на ранніх стадіях, може бути забуте, коли щось зміниться пізніше, що призведе до непередбаченої проблеми.

Для надійності зазвичай краще працювати з буферами фіксованого розміру, де це можливо, і розробити ескіз, щоб явно працювати з цими межами з самого початку. Це означає, що будь-які майбутні зміни в ескізі або будь-які несподівані обставини під час виконання роботи, сподіваємось, не повинні викликати проблем із пам'яттю.


6

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

Приклад: У мене є клас послідовних пакетів (бібліотека), який може приймати корисні навантаження даних по будь-якій довжині (може бути структура, масив 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 з флеш-пам’яттю 32 КБ ISP, 1024B EEPROM та 2 КБ SRAM. Це не багато ресурсів пам'яті.

Пам'ятайте, що 2KB SRAM використовується для всіх глобальних змінних, рядкових літералів, стека та можливого використання купи. Стек також повинен мати головний простір для ISR.

Розподіл пам'яті є:

Карта SRAM

Сьогоднішні ПК / ноутбуки мають більш ніж 1.000.000 разів більше пам'яті. Простір стека за замовчуванням 1 Мбайт на потік не є рідкістю, але абсолютно нереально для MPU.

Вбудований програмний проект повинен робити бюджет ресурсів. Це оцінка затримки ISR, необхідного простору пам’яті, обчислювальної потужності, циклів інструкцій тощо. На жаль, немає вільних обідів, і важке вбудоване програмування в режимі реального часу є найскладнішим з навичок програмування для оволодіння.


Амінь на це: "[H] ard вбудоване програмування в реальному часі - це найскладніші навички програмування.
StuffAndyMakes

Чи завжди час виконання malloc завжди однаковий? Я можу собі уявити, що malloc займає більше часу, коли він шукає подальший банер у доступному слоті, який підходить? Це був би ще один аргумент (окрім закінчення барана) не виділяти пам'ять на ходу?
Павло

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

0

Гаразд, я знаю, що це давнє питання, але чим більше я читаю відповіді, тим більше я продовжую повертатися до спостереження, яке здається помітним.

Проблема зупинки справжня

Здається, тут є посилання з проблемою зупинки Тьюрінга. Дозвіл динамічного розподілу збільшує шанси зазначеного «зупинення», тому питання стає одним із питань толерантності до ризику. Хоча зручно відмовитись від malloc()невдачі і т. Д., Це все-таки є достовірним результатом. Питання, яке задає ОП, стосується лише техніки, і так, важлива інформація про використовувані бібліотеки або конкретний MPU; розмова перетворюється на зменшення ризику зупинки програми або будь-якого іншого аномального завершення. Нам потрібно визнати існування середовищ, які переносять ризик абсолютно по-різному. Мій проект хобі відобразити красиві кольори на світлодіодній стрічці не вб’є когось, якщо трапиться щось незвичне, але MCU всередині машини з легким серцем, швидше за все, буде.

Привіт, містер Тюрінг Мене звуть Хубріс

Що стосується моєї світлодіодної стрічки, мені байдуже, чи вона замикається, я її просто скинув. Якби я був на машині серця-легенів, контрольованої MCU, наслідки його замикання або відмови від роботи - це буквально життя і смерть, тому питання про malloc()та, як free()слід розбиватись, полягає в тому, як передбачається програма вирішує можливість демонстрації містера. Відома проблема Тьюрінга. Неважко забути, що це математичний доказ і переконати себе, що якщо тільки ми досить розумні, ми можемо уникнути втрати меж обчислень.

На це запитання повинні бути дві прийняті відповіді: один для тих, хто змушений моргати, коли дивиться «Проблема зупинки» в обличчя, і один для всіх інших. Незважаючи на те, що більшість застосувань ардуїно, ймовірно, не є критично важливими програмами або програмами, що стосуються життя та смерті, відмінність все ще існує незалежно від MPU, який ви можете кодувати.


Я не думаю, що проблема зупинки застосовується в цій конкретній ситуації, враховуючи той факт, що використання купи не обов'язково є довільним. Якщо використовувати чітко визначений спосіб, то використання купи стає передбачувано "безпечним". Суть проблеми Холтінга з'ясувала, чи можна визначити, що відбувається з обов'язково довільним і не дуже чітко визначеним алгоритмом. Це дійсно набагато більше стосується програмування в більш широкому сенсі, і я вважаю, що це особливо не дуже актуально. Я навіть не думаю, що це взагалі актуально, щоб бути повністю чесним.
Джонатан Грей

Я визнаю деяке риторичне перебільшення, але справа в тому, що якщо ви хочете гарантувати поведінку, використання купи означає, що рівень ризику набагато вищий, ніж дотримуватися лише використання стека.
Келлі С. Французька

-3

Ні, але їх потрібно використовувати дуже обережно щодо звільнення виділеної пам'яті. Я ніколи не розумів, чому люди кажуть, що слід уникати прямого управління пам'яттю, оскільки це означає рівень некомпетентності, який взагалі несумісний з розробкою програмного забезпечення.

Скажімо, ви використовуєте свій ардуїно для управління безпілотником. Будь-яка помилка в будь-якій частині вашого коду потенційно може призвести до того, що він впаде з неба і пошкодить когось або щось. Іншими словами, якщо комусь не вистачає компетенцій щодо використання 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.