Використання malloc()
і free()
здається досить рідкісним у світі Ардуїно. Він використовується в чистому AVR C набагато частіше, але все ж з обережністю.
Це дійсно погана ідея використовувати malloc()
і free()
з Arduino?
Використання malloc()
і free()
здається досить рідкісним у світі Ардуїно. Він використовується в чистому AVR C набагато частіше, але все ж з обережністю.
Це дійсно погана ідея використовувати malloc()
і free()
з Arduino?
Відповіді:
Моє загальне правило для вбудованих систем - це лише malloc()
великі буфери і лише один раз, на початку програми, наприклад, в setup()
. Проблема виникає, коли ви розподіляєте та розподіляєте пам'ять. Під час тривалого сеансу пам'ять стає фрагментарною, і з часом розподіл не вдається через відсутність достатньо великої вільної області, навіть якщо загальна вільна пам'ять є більш ніж достатньою для запиту.
(Історична перспектива, пропустити, якщо не цікавить): Залежно від реалізації завантажувача, єдиною перевагою розподілу часу виконання порівняно з розподілом часу компіляції (інтилізовані глобальні точки) є розмір шістнадцяткового файлу. Коли вбудовані системи були побудовані за допомогою нестандартних комп'ютерів, що мають всю енергонезалежну пам'ять, програма часто завантажувалася до вбудованої системи з мережі або приладового комп'ютера, і час завантаження іноді був проблемою. Якщо залишити буфери, наповнені нулями на зображенні, це може значно скоротити час.)
Якщо мені потрібен динамічний розподіл пам’яті у вбудованій системі, я, як правило malloc()
, або бажано статично виділити великий пул і розділити його на буфери фіксованого розміру (або по одному пулу кожного з малих і великих буферів відповідно) і робити власне виділення / де-виділення з цього пулу. Тоді кожен запит на будь-який об'єм пам'яті до фіксованого розміру буфера задовольняється одним із цих буферів. Функція виклику не повинна знати, чи більша вона від запитуваної, і, уникаючи розщеплення та повторного поєднання блоків, ми вирішуємо фрагментацію. Звичайно, витоки пам’яті все ще можуть відбуватися, якщо програма виділяє / де-виділяє помилки.
Як правило, при написанні 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
видаленні екземпляра.
Я переглянув алгоритм, який використовує malloc()
avr-libc, і, здається, є кілька моделей використання, безпечних з точки зору фрагментації купи:
Я маю на увазі: виділяйте все, що вам потрібно на початку програми, і ніколи не звільняйте її. Звичайно, у цьому випадку ви також можете використовувати статичні буфери ...
Значення: ви звільняєте буфер, перш ніж виділяти щось інше. Розумний приклад може виглядати так:
void foo()
{
size_t size = figure_out_needs();
char * buffer = malloc(size);
if (!buffer) fail();
do_whatever_with(buffer);
free(buffer);
}
Якщо всередину немає молотка do_whatever_with()
, або якщо ця функція звільняє все, що виділяється, ви захищені від фрагментації.
Це узагальнення двох попередніх випадків. Якщо ви використовуєте купу як стек (останній перший спочатку), він буде вести себе як стек, а не фрагмент. Слід зазначити, що в цьому випадку безпечно змінити розмір останнього виділеного буфера за допомогою realloc()
.
Це не запобіжить фрагментації, але це безпечно в тому сенсі, що купа не зросте більше, ніж максимально використаний розмір. Якщо всі ваші буфери мають однаковий розмір, ви можете бути впевнені, що кожного разу, коли ви звільните один із них, слот буде доступний для наступних розподілів.
Використання динамічного розподілу (через malloc
/ free
або new
/ delete
) само по собі не є поганим. Насправді, для чогось подібного обробку рядків (наприклад, через String
об'єкт), це часто досить корисно. Це тому, що багато ескізів використовують кілька невеликих фрагментів струн, які з часом об'єднуються у більші. Використання динамічного розподілу дозволяє використовувати лише стільки пам’яті, скільки потрібно для кожного. Навпаки, використання статичного буфера фіксованого розміру для кожного може призвести до того, що витрачається багато місця (змушує його вичерпати пам'ять набагато швидше), хоча це повністю залежить від контексту.
З урахуванням цього, дуже важливо переконатися, що використання пам'яті передбачувано. Дозволення ескізу використовувати довільну кількість пам'яті залежно від обставин виконання (наприклад, введення) може рано чи пізно спричинити проблему. У деяких випадках це може бути абсолютно безпечно, наприклад, якщо ви знаєте, що використання ніколи не зробить багато. Ескізи можуть змінюватися в процесі програмування. Припущення, зроблене на ранніх стадіях, може бути забуте, коли щось зміниться пізніше, що призведе до непередбаченої проблеми.
Для надійності зазвичай краще працювати з буферами фіксованого розміру, де це можливо, і розробити ескіз, щоб явно працювати з цими межами з самого початку. Це означає, що будь-які майбутні зміни в ескізі або будь-які несподівані обставини під час виконання роботи, сподіваємось, не повинні викликати проблем із пам'яттю.
Я не погоджуюся з людьми, які вважають, що ви не повинні його використовувати, або це взагалі непотрібно. Я вважаю, що це може бути небезпечно, якщо ви не знаєте плюсів і прав, але це корисно. У мене є випадки, коли я не знаю (і мені не байдуже знати) розмір структури або буфера (під час компіляції або часу виконання), особливо коли мова йде про бібліотеки, які я надсилаю у світ. Я погоджуюся, що якщо у вашому додатку є лише одна відома структура, вам слід просто запустити цей розмір під час компіляції.
Приклад: У мене є клас послідовних пакетів (бібліотека), який може приймати корисні навантаження даних по будь-якій довжині (може бути структура, масив 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 з флеш-пам’яттю 32 КБ ISP, 1024B EEPROM та 2 КБ SRAM. Це не багато ресурсів пам'яті.
Пам'ятайте, що 2KB SRAM використовується для всіх глобальних змінних, рядкових літералів, стека та можливого використання купи. Стек також повинен мати головний простір для ISR.
Сьогоднішні ПК / ноутбуки мають більш ніж 1.000.000 разів більше пам'яті. Простір стека за замовчуванням 1 Мбайт на потік не є рідкістю, але абсолютно нереально для MPU.
Вбудований програмний проект повинен робити бюджет ресурсів. Це оцінка затримки ISR, необхідного простору пам’яті, обчислювальної потужності, циклів інструкцій тощо. На жаль, немає вільних обідів, і важке вбудоване програмування в режимі реального часу є найскладнішим з навичок програмування для оволодіння.
Гаразд, я знаю, що це давнє питання, але чим більше я читаю відповіді, тим більше я продовжую повертатися до спостереження, яке здається помітним.
Здається, тут є посилання з проблемою зупинки Тьюрінга. Дозвіл динамічного розподілу збільшує шанси зазначеного «зупинення», тому питання стає одним із питань толерантності до ризику. Хоча зручно відмовитись від malloc()
невдачі і т. Д., Це все-таки є достовірним результатом. Питання, яке задає ОП, стосується лише техніки, і так, важлива інформація про використовувані бібліотеки або конкретний MPU; розмова перетворюється на зменшення ризику зупинки програми або будь-якого іншого аномального завершення. Нам потрібно визнати існування середовищ, які переносять ризик абсолютно по-різному. Мій проект хобі відобразити красиві кольори на світлодіодній стрічці не вб’є когось, якщо трапиться щось незвичне, але MCU всередині машини з легким серцем, швидше за все, буде.
Що стосується моєї світлодіодної стрічки, мені байдуже, чи вона замикається, я її просто скинув. Якби я був на машині серця-легенів, контрольованої MCU, наслідки його замикання або відмови від роботи - це буквально життя і смерть, тому питання про malloc()
та, як free()
слід розбиватись, полягає в тому, як передбачається програма вирішує можливість демонстрації містера. Відома проблема Тьюрінга. Неважко забути, що це математичний доказ і переконати себе, що якщо тільки ми досить розумні, ми можемо уникнути втрати меж обчислень.
На це запитання повинні бути дві прийняті відповіді: один для тих, хто змушений моргати, коли дивиться «Проблема зупинки» в обличчя, і один для всіх інших. Незважаючи на те, що більшість застосувань ардуїно, ймовірно, не є критично важливими програмами або програмами, що стосуються життя та смерті, відмінність все ще існує незалежно від MPU, який ви можете кодувати.
Ні, але їх потрібно використовувати дуже обережно щодо звільнення виділеної пам'яті. Я ніколи не розумів, чому люди кажуть, що слід уникати прямого управління пам'яттю, оскільки це означає рівень некомпетентності, який взагалі несумісний з розробкою програмного забезпечення.
Скажімо, ви використовуєте свій ардуїно для управління безпілотником. Будь-яка помилка в будь-якій частині вашого коду потенційно може призвести до того, що він впаде з неба і пошкодить когось або щось. Іншими словами, якщо комусь не вистачає компетенцій щодо використання malloc, вони, ймовірно, взагалі не повинні кодувати, оскільки існує так багато інших областей, де невеликі помилки можуть спричинити серйозні проблеми.
Чи помилки, спричинені маликом, важче відстежити та виправити? Так, але це швидше розчарування з боку кодерів, а не ризик. Що стосується ризику, будь-яка частина вашого коду може бути рівною або ризикованою, ніж malloc, якщо ви не вживаєте заходів, щоб переконатися, що це зроблено правильно.