Apakah menggunakan malloc () dan gratis () ide yang sangat buruk di Arduino?

49

Penggunaan malloc()dan free()tampaknya cukup langka di dunia Arduino. Ini digunakan dalam AVR C murni jauh lebih sering, tetapi masih dengan hati-hati.

Apakah ini ide yang sangat buruk untuk digunakan malloc()dan free()dengan Arduino?

Cybergibbons
sumber
2
Anda akan kehabisan memori sangat cepat jika tidak, dan jika Anda tahu berapa banyak memori yang akan Anda gunakan, Anda mungkin juga mengalokasikannya secara statis
ratchet freak
1
Saya tidak tahu apakah itu buruk , tapi saya pikir itu tidak digunakan karena Anda hampir tidak pernah kehabisan RAM untuk sebagian besar sketsa dan itu hanya membuang-buang flash dan siklus clock yang berharga. Juga, jangan lupa tentang ruang lingkup (walaupun saya tidak tahu apakah ruang itu masih dialokasikan untuk semua variabel).
Penguin Anonim
4
Seperti biasa, jawaban yang tepat adalah "itu tergantung." Anda belum memberikan informasi yang cukup untuk mengetahui dengan pasti apakah alokasi dinamis tepat untuk Anda.
WineSoaked

Jawaban:

40

Aturan umum saya untuk sistem embedded adalah hanya malloc()buffer besar dan hanya sekali, pada awal program, misalnya di setup(). Masalahnya muncul ketika Anda mengalokasikan dan mendelegasikan memori. Selama sesi jangka panjang, memori menjadi terfragmentasi dan akhirnya alokasi gagal karena kurangnya area bebas yang cukup besar, meskipun total memori bebas lebih dari cukup untuk permintaan.

(Perspektif historis, lewati jika tidak tertarik): Bergantung pada implementasi loader, satu-satunya keuntungan dari alokasi run-time vs alokasi waktu kompilasi (globalisasi global) adalah ukuran file hex. Ketika sistem tertanam dibangun dengan komputer yang memiliki semua memori tidak stabil, program sering diunggah ke sistem tertanam dari jaringan atau komputer instrumentasi dan waktu pengunggahan terkadang menjadi masalah. Meninggalkan buffer yang penuh dengan nol dari gambar dapat mempersingkat waktu.)

Jika saya membutuhkan alokasi memori dinamis dalam sistem tertanam, saya umumnya malloc(), atau lebih disukai, mengalokasikan secara statis, kumpulan besar dan membaginya menjadi buffer ukuran tetap (atau masing-masing satu kumpulan buffer kecil dan besar, masing-masing) dan melakukan alokasi saya sendiri / de-alokasi dari kumpulan itu. Kemudian setiap permintaan untuk jumlah memori berapa pun hingga ukuran buffer tetap dihormati dengan salah satu buffer itu. Fungsi panggilan tidak perlu tahu apakah itu lebih besar dari yang diminta, dan dengan menghindari pemisahan dan penggabungan ulang blok kami memecahkan fragmentasi. Tentu saja kebocoran memori masih dapat terjadi jika program telah mengalokasikan / menghapus alokasi bug.

JRobert
sumber
Catatan sejarah lain, ini dengan cepat mengarah ke segmen BSS, yang memungkinkan program untuk nol ingatannya sendiri untuk inisialisasi, tanpa perlahan-lahan menyalin nol selama beban program.
rsaxvc
16

Biasanya, saat menulis sketsa Arduino, Anda akan menghindari alokasi dinamis (baik dengan mallocatau newuntuk contoh C ++), orang lebih suka menggunakan staticvariabel global atau variabel, atau variabel lokal (tumpukan).

Menggunakan alokasi dinamis dapat menyebabkan beberapa masalah:

  • kebocoran memori (jika Anda kehilangan pointer ke memori yang sebelumnya Anda alokasikan, atau lebih mungkin jika Anda lupa untuk membebaskan memori yang dialokasikan ketika Anda tidak membutuhkannya lagi)
  • heap fragmentation (setelah beberapa malloc/ freepanggilan) di mana heap tumbuh lebih besar dari jumlah sebenarnya memori yang dialokasikan saat ini

Dalam sebagian besar situasi yang saya hadapi, alokasi dinamis tidak diperlukan, atau dapat dihindari dengan makro seperti dalam contoh kode berikut:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

class Dummy
{
    byte buffer[BUFFER_SIZE];
    ...
};

Tanpa #define BUFFER_SIZE, jika kita ingin Dummykelas memiliki ukuran yang tidak tetap buffer, kita harus menggunakan alokasi dinamis sebagai berikut:

class Dummy
{
    const byte* buffer;

    public:
    Dummy(int size):buffer(new byte[size])
    {
    }

    ~Dummy()
    {
        delete [] bufer;
    }
};

Dalam hal ini, kami memiliki lebih banyak opsi daripada sampel pertama (misalnya menggunakan Dummyobjek yang berbeda dengan bufferukuran yang berbeda untuk masing-masing), tetapi kami mungkin memiliki masalah tumpukan fragmentasi.

Perhatikan penggunaan destruktor untuk memastikan memori yang dialokasikan secara dinamis untuk bufferakan dibebaskan ketika Dummyinstance dihapus.

jfpoilpret
sumber
14

Saya telah melihat algoritma yang digunakan oleh malloc(), dari avr-libc, dan tampaknya ada beberapa pola penggunaan yang aman dari sudut pandang tumpukan fragmentasi:

1. Alokasikan hanya buffer yang berumur panjang

Maksud saya: alokasikan semua yang Anda butuhkan di awal program, dan jangan pernah membebaskannya. Tentu saja, dalam hal ini, Anda juga dapat menggunakan buffer statis ...

2. Alokasikan hanya buffer berumur pendek

Artinya: Anda membebaskan buffer sebelum mengalokasikan apa pun. Contoh yang masuk akal mungkin terlihat seperti ini:

void foo()
{
    size_t size = figure_out_needs();
    char * buffer = malloc(size);
    if (!buffer) fail();
    do_whatever_with(buffer);
    free(buffer);
}

Jika tidak ada malloc di dalam do_whatever_with(), atau jika fungsi itu membebaskan apa pun yang dialokasikan, maka Anda aman dari fragmentasi.

3. Selalu bebaskan buffer yang dialokasikan terakhir

Ini adalah generalisasi dari dua kasus sebelumnya. Jika Anda menggunakan tumpukan seperti tumpukan (masuk pertama keluar), maka ia akan berperilaku seperti tumpukan dan bukan fragmen. Perlu dicatat bahwa dalam hal ini aman untuk mengubah ukuran buffer yang dialokasikan terakhir realloc().

4. Selalu alokasikan ukuran yang sama

Ini tidak akan mencegah fragmentasi, tetapi aman dalam arti bahwa tumpukan tidak akan tumbuh lebih besar dari ukuran maksimum yang digunakan . Jika semua buffer Anda memiliki ukuran yang sama, Anda dapat yakin bahwa, kapan pun Anda membebaskannya, slot akan tersedia untuk alokasi selanjutnya.

Edgar Bonet
sumber
1
Pola 2 harus dihindari karena menambahkan siklus untuk malloc () dan gratis () ketika ini dapat dilakukan dengan "char buffer [size];" (dalam C ++). Saya juga ingin menambahkan anti-pola "Never from a ISR".
Mikael Patel
9

Menggunakan alokasi dinamis (via malloc/ freeatau new/ delete) pada dasarnya tidak seburuk itu. Bahkan, untuk sesuatu seperti pemrosesan string (misalnya melalui Stringobjek), seringkali cukup membantu. Itu karena banyak sketsa menggunakan beberapa fragmen kecil string, yang akhirnya digabungkan menjadi yang lebih besar. Menggunakan alokasi dinamis memungkinkan Anda menggunakan hanya memori sebanyak yang Anda butuhkan untuk masing-masing memori. Sebaliknya, menggunakan buffer statis ukuran tetap untuk masing-masingnya dapat menghabiskan banyak ruang (menyebabkan kehabisan memori lebih cepat), meskipun itu sepenuhnya tergantung pada konteksnya.

Dengan semua itu dikatakan, sangat penting untuk memastikan penggunaan memori dapat diprediksi. Mengizinkan sketsa untuk menggunakan jumlah memori sewenang-wenang tergantung pada kondisi run-time (misalnya input) dapat dengan mudah menyebabkan masalah cepat atau lambat. Dalam beberapa kasus, ini mungkin sangat aman, misalnya jika Anda tahu penggunaannya tidak akan bertambah banyak. Sketsa dapat berubah selama proses pemrograman. Asumsi yang dibuat sejak awal bisa dilupakan ketika sesuatu diubah kemudian, menghasilkan masalah yang tidak terduga.

Untuk ketahanan, biasanya lebih baik bekerja dengan buffer ukuran tetap jika memungkinkan, dan merancang sketsa untuk bekerja secara eksplisit dengan batas-batas tersebut sejak awal. Itu berarti setiap perubahan sketsa di masa depan, atau keadaan run-time yang tidak terduga, semoga tidak menyebabkan masalah memori.

Peter Bloomfield
sumber
6

Saya tidak setuju dengan orang-orang yang berpikir Anda tidak boleh menggunakannya atau itu umumnya tidak perlu. Saya percaya ini bisa berbahaya jika Anda tidak mengetahui seluk beluknya, tetapi ini berguna. Saya memang memiliki kasus di mana saya tidak tahu (dan seharusnya tidak peduli untuk mengetahui) ukuran struktur atau buffer (pada waktu kompilasi atau run time), terutama ketika datang ke perpustakaan yang saya kirim ke dunia. Saya setuju bahwa jika aplikasi Anda hanya berurusan dengan struktur tunggal yang diketahui, Anda harus memanggang ukuran itu pada waktu kompilasi.

Contoh: Saya memiliki kelas paket serial (pustaka) yang dapat mengambil payload data panjang sewenang-wenang (bisa berupa struct, array dari uint16_t, dll.). Pada akhir pengiriman kelas itu, Anda cukup memberi tahu metode Packet.send () alamat benda yang ingin Anda kirim dan port HardwareSerial tempat Anda ingin mengirimkannya. Namun, pada sisi penerima saya membutuhkan buffer penerima yang dialokasikan secara dinamis untuk menahan muatan yang masuk, karena muatan itu bisa menjadi struktur yang berbeda pada saat tertentu, misalnya, tergantung pada kondisi aplikasi. JIKA saya hanya pernah mengirim struktur tunggal bolak-balik, saya hanya akan membuat buffer ukuran yang dibutuhkan pada waktu kompilasi. Tetapi, dalam kasus di mana paket mungkin berbeda panjang dari waktu ke waktu, malloc () dan free () tidak terlalu buruk.

Saya sudah menjalankan tes dengan kode berikut selama berhari-hari, membiarkannya berulang terus, dan saya tidak menemukan bukti fragmentasi memori. Setelah membebaskan memori yang dialokasikan secara dinamis, jumlah bebas kembali ke nilai sebelumnya.

// 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
}

Saya belum melihat adanya penurunan dalam RAM atau dalam kemampuan saya untuk mengalokasikannya secara dinamis menggunakan metode ini, jadi saya akan mengatakan itu adalah alat yang layak. FWIW.

StuffAndyMakes
sumber
2
Kode tes Anda sesuai dengan pola penggunaan 2. Alokasikan hanya buffer berumur pendek yang saya jelaskan dalam jawaban saya sebelumnya. Ini adalah salah satu dari beberapa pola penggunaan yang diketahui aman.
Edgar Bonet
Dengan kata lain, masalah akan muncul ketika Anda mulai berbagi prosesor dengan kode tidak dikenal lainnya - yang justru merupakan masalah yang Anda pikir Anda hindari. Secara umum, jika Anda menginginkan sesuatu yang akan selalu berfungsi atau gagal selama penautan, Anda membuat alokasi tetap ukuran maksimum dan menggunakannya berulang-ulang, misalnya dengan meminta pengguna Anda memberikannya kepada Anda dalam inisialisasi. Ingat Anda biasanya menjalankan chip yang semuanya harus sesuai dengan 2048 byte - mungkin lebih pada beberapa papan tetapi juga mungkin jauh lebih sedikit pada yang lain.
Chris Stratton
@ EdgarBonet Ya, tepatnya. Hanya ingin berbagi.
StuffAndyMakes
1
Mengalokasikan buffer secara dinamis hanya dengan ukuran yang diperlukan berisiko, seolah-olah ada hal lain yang dialokasikan sebelum Anda membebaskannya, Anda dapat dibiarkan dengan fragmentasi - memori yang tidak dapat Anda gunakan kembali. Juga, alokasi dinamis memiliki overhead pelacakan. Alokasi tetap tidak berarti Anda tidak dapat memperbanyak menggunakan memori, itu hanya berarti bahwa Anda harus mengerjakan pembagian ke dalam desain program Anda. Untuk buffer dengan ruang lingkup murni lokal, Anda mungkin juga mempertimbangkan penggunaan tumpukan. Anda belum memeriksa kemungkinan malloc () gagal.
Chris Stratton
1
"Itu bisa berbahaya jika kamu tidak mengetahui seluk beluknya, tetapi ini berguna." cukup banyak meringkas semua pengembangan dalam C / C ++. :-)
ThatAintWorking
4

Apakah itu ide yang sangat buruk untuk menggunakan malloc () dan gratis () dengan Arduino?

Jawaban singkatnya adalah ya. Berikut alasannya:

Ini semua tentang memahami apa itu MPU dan bagaimana memprogram dalam batasan sumber daya yang tersedia. The Arduino Uno menggunakan MPU ATmega328p dengan memori flash ISP 32KB, EEPROM 1024B, dan SRAM 2KB. Itu tidak banyak sumber daya memori.

Ingat bahwa SRAM 2KB digunakan untuk semua variabel global, string literal, stack, dan kemungkinan penggunaan heap. Tumpukan juga perlu memiliki ruang kepala untuk ISR.

The tata letak memori adalah:

Peta SRAM

Saat ini PC / laptop memiliki lebih dari 1.000.000 kali jumlah memori. Ruang stack default 1 Mbyte per utas tidak jarang tetapi sama sekali tidak realistis pada MPU.

Proyek perangkat lunak tertanam harus melakukan anggaran sumber daya. Ini memperkirakan latensi ISR, ruang memori yang diperlukan, daya hitung, siklus instruksi, dll. Sayangnya tidak ada makan siang gratis dan pemrograman tersemat real-time adalah keterampilan pemrograman yang paling sulit untuk dikuasai.

Mikael Patel
sumber
Amin untuk itu: "[H] pada pemrograman embedded real-time adalah keterampilan pemrograman yang paling sulit untuk dikuasai."
StuffAndyMakes
Apakah waktu pelaksanaan malloc selalu sama? Saya bisa membayangkan malloc mengambil lebih banyak waktu karena mencari lebih lanjut di ram yang tersedia untuk slot yang cocok? Ini akan menjadi argumen lain (selain kehabisan ram) untuk tidak mengalokasikan memori saat bepergian?
Paul
@ Paa Algoritma heap (malloc dan gratis) biasanya bukan waktu eksekusi yang konstan, dan tidak reentrant. Algoritme berisi struktur pencarian dan data yang memerlukan kunci saat menggunakan utas (konkurensi).
Mikael Patel
0

Ok, saya tahu ini adalah pertanyaan lama tetapi semakin saya membaca jawaban semakin saya terus kembali ke pengamatan yang tampaknya menonjol.

Masalah Pemutusan Itu Nyata

Tampaknya ada tautan dengan Masalah Puting Turing di sini. Mengizinkan alokasi dinamis meningkatkan peluang kata 'terhenti' sehingga pertanyaannya menjadi toleransi risiko. Meskipun nyaman untuk menghilangkan kemungkinan malloc()gagal dan sebagainya, itu masih merupakan hasil yang valid. Pertanyaan yang ditanyakan OP hanya nampak tentang teknik, dan ya detail perpustakaan yang digunakan atau MPU spesifik memang penting; percakapan berubah ke arah mengurangi risiko terhentinya program atau akhir yang tidak normal lainnya. Kita perlu mengenali keberadaan lingkungan yang mentolerir risiko sangat berbeda. Proyek hobi saya untuk menampilkan warna-warna cantik pada strip-LED tidak akan membunuh seseorang jika sesuatu yang tidak biasa terjadi tetapi MCU di dalam mesin jantung-paru kemungkinan akan melakukannya.

Halo, Turing Nama Saya Hubris

Untuk strip-LED saya, saya tidak peduli jika terkunci, saya hanya akan mengatur ulang. Jika saya menggunakan mesin jantung-paru yang dikendalikan oleh MCU, konsekuensi dari pengunciannya atau gagal beroperasi adalah benar-benar hidup dan mati, jadi pertanyaan tentang malloc()dan free()harus dipisah antara bagaimana program yang dimaksud menangani kemungkinan menunjukkan Tn. Masalah terkenal Turing. Mungkin mudah untuk melupakan bahwa itu adalah bukti matematis dan meyakinkan diri kita bahwa jika kita cukup pintar kita dapat menghindari menjadi korban dari batas perhitungan.

Pertanyaan ini seharusnya memiliki dua jawaban yang diterima, satu untuk mereka yang dipaksa untuk berkedip ketika menatap Masalah Henti di wajah, dan satu untuk yang lainnya. Sementara sebagian besar penggunaan arduino kemungkinan bukan misi kritis atau aplikasi hidup dan mati, perbedaannya masih ada terlepas dari MPU mana yang Anda kodekan.

Kelly S. Prancis
sumber
Saya tidak berpikir masalah Hentikan berlaku dalam situasi khusus ini mengingat fakta bahwa penggunaan tumpukan tidak selalu sewenang-wenang. Jika digunakan dengan cara yang terdefinisi dengan baik maka tumpukan penggunaan dapat diprediksi "aman". Inti dari masalah Hentikan adalah mencari tahu apakah dapat ditentukan apa yang terjadi pada algoritma yang sewenang-wenang dan tidak didefinisikan dengan baik. Ini benar-benar berlaku jauh lebih untuk pemrograman dalam arti yang lebih luas dan karena itu saya merasa secara khusus tidak terlalu relevan di sini. Saya bahkan tidak berpikir itu relevan sama sekali untuk jujur ​​sepenuhnya.
Jonathan Gray
Saya akan mengakui beberapa retorika berlebihan tetapi intinya adalah benar-benar jika Anda ingin menjamin perilaku, menggunakan heap menyiratkan tingkat risiko yang jauh lebih tinggi daripada bertahan hanya menggunakan stack.
Kelly S. French
-3

Tidak, tetapi mereka harus digunakan dengan sangat hati-hati dalam hal membebaskan () memori yang dialokasikan. Saya tidak pernah mengerti mengapa orang mengatakan manajemen memori langsung harus dihindari karena ini menyiratkan tingkat ketidakmampuan yang umumnya tidak sesuai dengan pengembangan perangkat lunak.

Katakanlah Anda menggunakan Arduino Anda untuk mengendalikan drone. Kesalahan apa pun di bagian mana pun dari kode Anda berpotensi menyebabkannya jatuh dari langit dan melukai seseorang atau sesuatu. Dengan kata lain, jika seseorang tidak memiliki kompetensi untuk menggunakan malloc, mereka kemungkinan tidak boleh mengkode sama sekali karena ada begitu banyak area lain di mana bug kecil dapat menyebabkan masalah serius.

Apakah bug yang disebabkan oleh malloc lebih sulit dilacak dan diperbaiki? Ya, tapi itu lebih merupakan masalah frustrasi pada bagian coders daripada risiko. Sejauh risiko berjalan, setiap bagian dari kode Anda dapat sama atau lebih berisiko daripada malloc jika Anda tidak mengambil langkah-langkah untuk memastikan itu dilakukan dengan benar.

JSON
sumber
4
Sangat menarik Anda menggunakan drone sebagai contoh. Menurut artikel ini ( mil-embedded.com/articles/… ), "Karena risikonya, alokasi memori dinamis dilarang, di bawah standar DO-178B, dalam kode avionik tertanam yang kritis terhadap keselamatan."
Gabriel Staples
DARPA memiliki sejarah panjang yang memungkinkan kontraktor untuk mengembangkan spesifikasi yang sesuai dengan platform mereka sendiri - mengapa mereka tidak harus ketika pembayar pajak yang membayar tagihan. Inilah sebabnya mengapa biaya $ 10 miliar bagi mereka untuk mengembangkan apa yang dapat dilakukan orang lain dengan $ 10.000. Hampir terdengar seolah-olah Anda menggunakan kompleks industri militer sebagai referensi yang jujur.
JSON
Alokasi dinamis tampaknya seperti undangan bagi program Anda untuk menunjukkan batas perhitungan yang dijelaskan dalam Masalah Pemutusan Hubungan. Ada beberapa lingkungan yang dapat menangani sejumlah kecil risiko penghentian tersebut dan ada lingkungan yang ada (ruang, pertahanan, medis, dll.) Yang tidak akan mentolerir sejumlah risiko yang dapat dikendalikan, sehingga mereka melarang operasi yang "tidak boleh" gagal karena 'itu harus bekerja' tidak cukup baik ketika Anda meluncurkan roket atau mengendalikan mesin jantung / paru-paru.
Kelly S. French