Việc sử dụng malloc () và free () có phải là một ý tưởng thực sự tồi tệ trên Arduino không?


49

Việc sử dụng malloc()free()dường như khá hiếm trong thế giới Arduino. Nó được sử dụng trong AVR C thuần túy thường xuyên hơn nhiều, nhưng vẫn thận trọng.

Nó có phải là một ý tưởng thực sự tồi để sử dụng malloc()free()với Arduino?


2
bạn sẽ hết bộ nhớ rất nhanh nếu không, và nếu bạn biết bạn sẽ sử dụng bao nhiêu bộ nhớ thì bạn cũng có thể phân bổ tĩnh cho nó
ratchet freak

1
Tôi không biết nó có tệ không , nhưng tôi nghĩ nó không được sử dụng vì hầu như bạn không bao giờ hết RAM cho hầu hết các bản phác thảo và nó chỉ là một sự lãng phí của đèn flash và chu kỳ đồng hồ quý giá. Ngoài ra, đừng quên phạm vi (mặc dù tôi không biết nếu không gian đó vẫn được phân bổ cho tất cả các biến).
Chim cánh cụt vô danh

4
Như thường lệ, câu trả lời đúng là "nó phụ thuộc." Bạn chưa cung cấp đủ thông tin để biết chắc chắn liệu phân bổ động có phù hợp với bạn hay không.
WineSoaken

Câu trả lời:


40

Quy tắc chung của tôi cho các hệ thống nhúng là chỉ malloc()các bộ đệm lớn và chỉ một lần, khi bắt đầu chương trình, ví dụ: trong setup(). Rắc rối xảy ra khi bạn phân bổ và phân bổ bộ nhớ. Trong một phiên chạy dài, bộ nhớ bị phân mảnh và cuối cùng việc phân bổ không thành công do thiếu một vùng trống đủ lớn, mặc dù tổng bộ nhớ trống là quá đủ cho yêu cầu.

(Phối cảnh lịch sử, bỏ qua nếu không quan tâm): Tùy thuộc vào việc triển khai trình tải, lợi thế duy nhất của phân bổ thời gian chạy so với phân bổ thời gian biên dịch (toàn cầu hóa) là kích thước của tệp hex. Khi các hệ thống nhúng được xây dựng với các máy tính có kệ có bộ nhớ dễ bay hơi, chương trình thường được tải lên hệ thống nhúng từ mạng hoặc máy tính thiết bị và thời gian tải lên đôi khi là một vấn đề. Loại bỏ bộ đệm đầy số không từ hình ảnh có thể rút ngắn thời gian đáng kể.)

Nếu tôi cần cấp phát bộ nhớ động trong một hệ thống nhúng, tôi thường malloc(), hoặc tốt nhất là phân bổ tĩnh, một nhóm lớn và chia nó thành các bộ đệm có kích thước cố định (hoặc một nhóm mỗi bộ đệm nhỏ và lớn) và thực hiện phân bổ riêng của tôi / phân bổ lại từ hồ bơi đó. Sau đó, mọi yêu cầu cho bất kỳ dung lượng bộ nhớ nào cho đến kích thước bộ đệm cố định đều được thực hiện với một trong những bộ đệm đó. Hàm gọi không cần biết liệu nó có lớn hơn yêu cầu hay không và bằng cách tránh chia tách và kết hợp lại các khối chúng ta giải quyết phân mảnh. Tất nhiên rò rỉ bộ nhớ vẫn có thể xảy ra nếu chương trình có lỗi cấp phát / giảm cấp phát.


Một ghi chú lịch sử khác, điều này nhanh chóng dẫn đến phân khúc BSS, cho phép một chương trình bằng không bộ nhớ của chính nó để khởi tạo, mà không cần sao chép chậm các số không trong khi tải chương trình.
rsaxvc

16

Thông thường, khi viết phác thảo Arduino, bạn sẽ tránh phân bổ động (có thể bằng mallochoặc newcho các phiên bản C ++), mọi người thay vì sử dụng biến toàn cục -or static- hoặc biến cục bộ (stack).

Sử dụng phân bổ động có thể dẫn đến một số vấn đề:

  • rò rỉ bộ nhớ (nếu bạn mất một con trỏ vào bộ nhớ bạn đã phân bổ trước đó hoặc nhiều khả năng nếu bạn quên giải phóng bộ nhớ được phân bổ khi bạn không cần nó nữa)
  • phân mảnh heap (sau vài lần malloc/ freecuộc gọi) trong đó heap phát triển lớn hơn số lượng bộ nhớ thực tế được phân bổ hiện tại

Trong hầu hết các tình huống tôi đã gặp phải, phân bổ động là không cần thiết hoặc có thể tránh được với các macro như trong mẫu mã sau:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Đồ ngốc

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

Nếu không #define BUFFER_SIZE, nếu chúng ta muốn Dummylớp có bufferkích thước không cố định , chúng ta sẽ phải sử dụng phân bổ động như sau:

class Dummy
{
    const byte* buffer;

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

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

Trong trường hợp này, chúng tôi có nhiều tùy chọn hơn trong mẫu đầu tiên (ví dụ: sử dụng các Dummyđối tượng khác nhau với bufferkích thước khác nhau cho từng đối tượng ), nhưng chúng tôi có thể có các vấn đề phân mảnh heap.

Lưu ý việc sử dụng một hàm hủy để đảm bảo bộ nhớ được cấp phát động buffersẽ được giải phóng khi một Dummythể hiện bị xóa.


14

Tôi đã xem xét thuật toán được sử dụng bởi malloc(), từ avr-libc, và dường như có một vài mô hình sử dụng an toàn theo quan điểm phân mảnh heap:

1. Chỉ phân bổ bộ đệm sống lâu

Ý tôi là: phân bổ tất cả những gì bạn cần khi bắt đầu chương trình, và không bao giờ giải phóng nó. Tất nhiên, trong trường hợp này, bạn cũng có thể sử dụng bộ đệm tĩnh ...

2. Chỉ phân bổ bộ đệm tồn tại trong thời gian ngắn

Ý nghĩa: bạn giải phóng bộ đệm trước khi phân bổ bất cứ thứ gì khác. Một ví dụ hợp lý có thể trông như thế này:

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

Nếu không có malloc bên trong do_whatever_with(), hoặc nếu chức năng đó giải phóng bất cứ thứ gì nó phân bổ, thì bạn an toàn khỏi bị phân mảnh.

3. Luôn giải phóng bộ đệm được phân bổ cuối cùng

Đây là một khái quát của hai trường hợp trước. Nếu bạn sử dụng heap như một stack (cuối cùng là trước hết), thì nó sẽ hoạt động như một stack và không phải là một đoạn. Cần lưu ý rằng trong trường hợp này, an toàn để thay đổi kích thước bộ đệm được phân bổ cuối cùng với realloc().

4. Luôn phân bổ cùng kích thước

Điều này sẽ không ngăn chặn sự phân mảnh, nhưng nó an toàn theo nghĩa là đống sẽ không phát triển lớn hơn kích thước tối đa được sử dụng . Nếu tất cả các bộ đệm của bạn có cùng kích thước, bạn có thể chắc chắn rằng, bất cứ khi nào bạn giải phóng một trong số chúng, vị trí sẽ có sẵn cho các lần phân bổ tiếp theo.


1
Nên tránh mẫu 2 vì nó thêm chu kỳ cho malloc () và free () khi điều này có thể được thực hiện với "char buffer [size];" (trong C ++). Tôi cũng muốn thêm mô hình chống "Không bao giờ từ ISR".
Mikael Patel

9

Sử dụng phân bổ động (thông qua malloc/ freehoặc new/ delete) vốn không tệ như vậy. Trong thực tế, đối với một cái gì đó như xử lý chuỗi (ví dụ thông qua Stringđối tượng), nó thường khá hữu ích. Đó là bởi vì nhiều bản phác thảo sử dụng một vài chuỗi nhỏ, cuối cùng được kết hợp thành một chuỗi lớn hơn. Sử dụng phân bổ động cho phép bạn chỉ sử dụng nhiều bộ nhớ mà bạn cần cho mỗi bộ nhớ. Ngược lại, sử dụng bộ đệm tĩnh có kích thước cố định cho mỗi bộ đệm có thể sẽ lãng phí rất nhiều dung lượng (khiến nó hết bộ nhớ nhanh hơn nhiều), mặc dù nó phụ thuộc hoàn toàn vào ngữ cảnh.

Với tất cả những gì đã nói, điều rất quan trọng là đảm bảo việc sử dụng bộ nhớ có thể dự đoán được. Cho phép bản phác thảo sử dụng số lượng bộ nhớ tùy ý tùy thuộc vào hoàn cảnh thời gian chạy (ví dụ: đầu vào) có thể dễ dàng gây ra sự cố sớm hay muộn. Trong một số trường hợp, nó có thể hoàn toàn an toàn, ví dụ: nếu bạn biết việc sử dụng sẽ không bao giờ tăng thêm nhiều. Phác thảo có thể thay đổi trong quá trình lập trình mặc dù. Một giả định được đưa ra sớm có thể bị lãng quên khi một cái gì đó được thay đổi sau đó, dẫn đến một vấn đề không lường trước được.

Để mạnh mẽ, thường tốt hơn khi làm việc với các bộ đệm có kích thước cố định nếu có thể và thiết kế bản phác thảo để hoạt động rõ ràng với các giới hạn đó ngay từ đầu. Điều đó có nghĩa là bất kỳ thay đổi nào trong tương lai đối với bản phác thảo, hoặc bất kỳ tình huống thời gian chạy bất ngờ nào, hy vọng sẽ không gây ra bất kỳ vấn đề nào về bộ nhớ.


6

Tôi không đồng ý với những người nghĩ rằng bạn không nên sử dụng nó hoặc nó thường không cần thiết. Tôi tin rằng nó có thể nguy hiểm nếu bạn không biết về nó, nhưng nó rất hữu ích. Tôi có những trường hợp mà tôi không biết (và không quan tâm để biết) kích thước của cấu trúc hoặc bộ đệm (tại thời gian biên dịch hoặc thời gian chạy), đặc biệt là khi nói đến các thư viện tôi gửi ra thế giới. Tôi đồng ý rằng nếu ứng dụng của bạn chỉ xử lý một cấu trúc duy nhất, đã biết, bạn chỉ nên nướng ở kích thước đó vào thời gian biên dịch.

Ví dụ: Tôi có một lớp gói nối tiếp (một thư viện) có thể lấy các tải trọng dữ liệu có độ dài tùy ý (có thể là struct, mảng của uint16_t, v.v.). Khi kết thúc việc gửi lớp đó, bạn chỉ cần nói cho phương thức Packet.send () địa chỉ của thứ bạn muốn gửi và cổng Phần cứng thông qua đó bạn muốn gửi nó. Tuy nhiên, ở đầu nhận, tôi cần một bộ đệm nhận được phân bổ động để giữ tải trọng đến đó, vì tải trọng đó có thể là một cấu trúc khác nhau tại bất kỳ thời điểm nào, tùy thuộc vào trạng thái của ứng dụng. NẾU tôi chỉ gửi một cấu trúc duy nhất qua lại, tôi chỉ làm cho bộ đệm có kích thước cần thiết trong thời gian biên dịch. Nhưng, trong trường hợp các gói có thể có độ dài khác nhau theo thời gian, malloc () và free () không quá tệ.

Tôi đã chạy thử nghiệm với mã sau trong nhiều ngày, để nó lặp lại liên tục và tôi không tìm thấy bằng chứng nào về sự phân mảnh bộ nhớ. Sau khi giải phóng bộ nhớ được cấp phát động, số tiền miễn phí sẽ trở về giá trị trước đó.

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

Tôi chưa thấy bất kỳ sự suy giảm nào trong RAM hoặc trong khả năng phân bổ nó một cách linh hoạt bằng phương pháp này, vì vậy tôi nói đó là một công cụ hữu hiệu. FWIW.


2
Mã kiểm tra của bạn phù hợp với mẫu sử dụng 2. Chỉ phân bổ các bộ đệm có thời gian sử dụng ngắn mà tôi đã mô tả trong câu trả lời trước. Đây là một trong số ít các mô hình sử dụng được biết là an toàn.
Edgar Bonet

Nói cách khác, các vấn đề sẽ xuất hiện khi bạn bắt đầu chia sẻ bộ xử lý với mã không xác định khác - đó chính xác là vấn đề bạn nghĩ rằng bạn đang tránh. Nói chung, nếu bạn muốn một cái gì đó luôn luôn hoạt động hoặc thất bại trong quá trình liên kết, bạn thực hiện phân bổ cố định kích thước tối đa và sử dụng nó nhiều lần, ví dụ như bằng cách người dùng của bạn chuyển nó cho bạn khi khởi tạo. Hãy nhớ rằng bạn thường chạy trên một con chip trong đó mọi thứ phải phù hợp với 2048 byte - có thể nhiều hơn trên một số bảng nhưng cũng có thể ít hơn trên các bảng khác.
Chris Stratton

@EdgarBonet Vâng, chính xác. Chỉ muốn chia sẻ.
StuffAndyMakes 19/05/2015

1
Việc tự động phân bổ một bộ đệm chỉ có kích thước cần thiết là rủi ro, vì nếu có bất cứ thứ gì khác phân bổ trước khi bạn giải phóng thì bạn có thể bị phân mảnh - bộ nhớ mà bạn không thể sử dụng lại. Ngoài ra, phân bổ động đã theo dõi chi phí. Phân bổ cố định không có nghĩa là bạn không thể sử dụng nhiều bộ nhớ, điều đó chỉ có nghĩa là bạn phải chia sẻ thiết kế chương trình của mình. Đối với một bộ đệm với phạm vi hoàn toàn cục bộ, bạn cũng có thể cân nhắc việc sử dụng ngăn xếp. Bạn chưa kiểm tra khả năng malloc () không thành công.
Chris Stratton

1
"nó có thể nguy hiểm nếu bạn không biết những thứ bên trong nó, nhưng nó rất hữu ích." khá nhiều tổng hợp tất cả sự phát triển trong C / C ++. :-)
ThatAintWorking

4

Có phải là một ý tưởng thực sự tồi khi sử dụng malloc () và free () với Arduino?

Câu trả lời ngắn gọn là có. Dưới đây là những lý do tại sao:

Đó là tất cả về việc hiểu MPU là gì và cách lập trình trong các ràng buộc của các tài nguyên có sẵn. Arduino Uno sử dụng MPU ATmega328p với bộ nhớ flash ISP 32KB, EEPROM 1024B và SRAM 2KB. Đó không phải là nhiều tài nguyên bộ nhớ.

Hãy nhớ rằng SRK 2KB được sử dụng cho tất cả các biến toàn cục, chuỗi ký tự, ngăn xếp và khả năng sử dụng của heap. Ngăn xếp cũng cần phải có phòng đầu cho ISR.

Các bố trí bộ nhớ là:

Bản đồ SRAM

Ngày nay PC / laptop có dung lượng bộ nhớ gấp hơn 1.000.000 lần. Không gian ngăn xếp mặc định 1 Mbyte cho mỗi luồng không phải là hiếm nhưng hoàn toàn không thực tế trên MPU.

Một dự án phần mềm nhúng phải làm một ngân sách tài nguyên. Đây là ước tính độ trễ ISR, không gian bộ nhớ cần thiết, sức mạnh tính toán, chu kỳ hướng dẫn, v.v ... Thật không may, không có bữa ăn trưa miễn phí và lập trình nhúng thời gian thực khó là kỹ năng lập trình khó nhất để thành thạo.


Amen cho rằng: "[H] ard lập trình nhúng thời gian thực là kỹ năng lập trình khó nhất để thành thạo."
StuffAndyMakes

Thời gian thực hiện của malloc luôn giống nhau? Tôi có thể tưởng tượng malloc mất nhiều thời gian hơn khi nó tìm kiếm thêm trong ram có sẵn cho một vị trí phù hợp? Đây sẽ là một đối số khác (ngoài việc hết ram) để không phân bổ bộ nhớ khi đang di chuyển?
Paul

@Paul Các thuật toán heap (malloc và miễn phí) thường không phải là thời gian thực hiện không đổi và không được phát hành lại. Thuật toán chứa các cấu trúc tìm kiếm và dữ liệu yêu cầu khóa khi sử dụng các luồng (đồng thời).
Mikael Patel

0

Ok, tôi biết đây là một câu hỏi cũ nhưng tôi càng đọc qua các câu trả lời, tôi càng tiếp tục quay lại với một quan sát có vẻ nổi bật.

Vấn đề dừng là có thật

Dường như có một mối liên hệ với vấn đề dừng của Turing ở đây. Cho phép phân bổ động làm tăng tỷ lệ nói 'tạm dừng' để câu hỏi trở thành một trong những chấp nhận rủi ro. Mặc dù thuận tiện để loại bỏ khả năng malloc()thất bại và vv, nó vẫn là một kết quả hợp lệ. Câu hỏi mà OP yêu cầu chỉ xuất hiện là về kỹ thuật, và vâng, các chi tiết của các thư viện được sử dụng hoặc MPU cụ thể có vấn đề; cuộc trò chuyện hướng tới việc giảm nguy cơ tạm dừng chương trình hoặc bất kỳ kết thúc bất thường nào khác. Chúng ta cần nhận ra sự tồn tại của môi trường chấp nhận rủi ro rất khác nhau. Dự án sở thích của tôi là hiển thị màu sắc đẹp trên dải đèn LED sẽ không giết ai đó nếu có điều gì đó bất thường xảy ra nhưng MCU bên trong một cỗ máy tim phổi có thể sẽ xảy ra.

Xin chào ông Turing Tên tôi là Hubris

Đối với dải đèn LED của tôi, tôi không quan tâm nếu nó bị khóa, tôi sẽ chỉ đặt lại nó. Nếu tôi sử dụng máy trợ tim do MCU điều khiển, hậu quả của việc khóa hoặc không hoạt động theo nghĩa đen là sự sống và cái chết, vì vậy câu hỏi về malloc()free()nên được phân chia giữa cách chương trình dự định đối phó với khả năng chứng minh Mr. Vấn đề nổi tiếng của Turing. Có thể dễ dàng quên rằng đó là một bằng chứng toán học và để thuyết phục bản thân rằng nếu chỉ cần chúng ta đủ thông minh, chúng ta có thể tránh được sự bất thường về giới hạn tính toán.

Câu hỏi này nên có hai câu trả lời được chấp nhận, một cho những người bị buộc phải chớp mắt khi nhìn chằm chằm vào Vấn đề dừng lại ở mặt và một cho tất cả những người khác. Mặc dù hầu hết việc sử dụng arduino có thể không thực hiện các ứng dụng quan trọng hoặc sinh tử, sự khác biệt vẫn còn đó bất kể bạn có thể mã hóa MPU nào.


Tôi không nghĩ rằng vấn đề Ngừng áp dụng trong tình huống cụ thể này khi xem xét thực tế rằng việc sử dụng heap không nhất thiết là tùy ý. Nếu được sử dụng theo cách được xác định rõ thì việc sử dụng heap sẽ trở nên "an toàn". Điểm của vấn đề Dừng là tìm hiểu xem liệu nó có thể được xác định điều gì xảy ra với một thuật toán nhất thiết tùy ý và không được xác định rõ ràng hay không. Nó thực sự áp dụng nhiều hơn cho lập trình theo nghĩa rộng hơn và như vậy tôi thấy nó đặc biệt không liên quan lắm ở đây. Tôi thậm chí không nghĩ rằng nó hoàn toàn có liên quan.
Jonathan Gray

Tôi sẽ thừa nhận một số cường điệu tu từ nhưng vấn đề thực sự là nếu bạn muốn đảm bảo hành vi, sử dụng heap ngụ ý mức độ rủi ro cao hơn nhiều so với việc chỉ sử dụng stack.
Kelly S. Pháp

-3

Không, nhưng chúng phải được sử dụng rất cẩn thận đối với bộ nhớ được cấp phát miễn phí (). Tôi chưa bao giờ hiểu tại sao mọi người nói nên tránh quản lý bộ nhớ trực tiếp vì nó hàm ý mức độ không đủ năng lực thường không tương thích với phát triển phần mềm.

Hãy nói rằng bạn sử dụng arduino của bạn để điều khiển máy bay không người lái. Bất kỳ lỗi nào trong bất kỳ phần nào trong mã của bạn đều có khả năng khiến nó rơi khỏi bầu trời và làm tổn thương ai đó hoặc điều gì đó. Nói cách khác, nếu ai đó thiếu khả năng sử dụng malloc, họ có thể không nên mã hóa chút nào vì có rất nhiều lĩnh vực khác mà các lỗi nhỏ có thể gây ra vấn đề nghiêm trọng.

Là các lỗi gây ra bởi malloc khó theo dõi và sửa chữa? Vâng, nhưng đó là vấn đề thất vọng nhiều hơn về phần mã hóa hơn là rủi ro. Theo như rủi ro, bất kỳ phần nào trong mã của bạn đều có thể rủi ro như nhau hoặc nhiều hơn so với malloc nếu bạn không thực hiện các bước để đảm bảo rằng nó được thực hiện đúng.


4
Thật thú vị khi bạn sử dụng máy bay không người lái làm ví dụ. Theo bài viết này ( mil-embedded.com/articles/và ), "Do rủi ro của nó, việc cấp phát bộ nhớ động bị cấm, theo tiêu chuẩn DO-178B, trong mã hàng không nhúng an toàn quan trọng."
Gabriel Staples

DARPA có một lịch sử lâu dài cho phép các nhà thầu phát triển các thông số kỹ thuật phù hợp với nền tảng của chính họ - tại sao họ không nên khi đó là những người nộp thuế trả hóa đơn. Đây là lý do tại sao chi phí 10 tỷ đô la để họ phát triển những gì người khác có thể làm với 10.000 đô la. Hầu như âm thanh của bạn sử dụng phức hợp công nghiệp quân sự như một tài liệu tham khảo trung thực.
JSON

Phân bổ động có vẻ như một lời mời cho chương trình của bạn để chứng minh các giới hạn tính toán được mô tả trong Vấn đề dừng. Có một số môi trường có thể xử lý một lượng nhỏ rủi ro tạm dừng như vậy và có những môi trường tồn tại (không gian, phòng thủ, y tế, v.v.) sẽ không chấp nhận bất kỳ rủi ro có thể kiểm soát nào, do đó họ không cho phép các hoạt động "không nên" thất bại vì 'nó nên hoạt động' không đủ tốt khi bạn phóng tên lửa hoặc điều khiển máy tim / phổi.
Kelly S. Pháp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.