Usar malloc () e free () é uma péssima idéia no Arduino?


49

O uso malloc()e free()parece bastante raro no mundo do Arduino. É usado no AVR C puro com muito mais frequência, mas ainda com cautela.

É uma péssima idéia usar malloc()e free()com o Arduino?


2
você vai ficar sem memória muito rápido de outra forma, e se você sabe o quanto de memória você vai usar assim como você pode estaticamente alocá-lo de qualquer maneira
catraca aberração

11
Não sei se é ruim , mas acho que não é usado porque você quase nunca fica sem memória RAM para a maioria dos esboços e é apenas um desperdício de flash e preciosos ciclos de relógio. Além disso, não se esqueça do escopo (embora eu não saiba se esse espaço ainda está alocado para todas as variáveis).
Anonymous Penguin

4
Como sempre, a resposta certa é "depende". Você não forneceu informações suficientes para saber com certeza se a alocação dinâmica é ideal para você.
precisa saber é o seguinte

Respostas:


40

Minha regra geral para sistemas embarcados é somente malloc()buffers grandes e apenas uma vez, no início do programa, por exemplo, em setup(). O problema ocorre quando você aloca e desaloca a memória. Em uma sessão de longo prazo, a memória fica fragmentada e, eventualmente, uma alocação falha devido à falta de uma área livre suficientemente grande, mesmo que a memória livre total seja mais que adequada para a solicitação.

(Perspectiva histórica, pule se não estiver interessado): Dependendo da implementação do carregador, a única vantagem da alocação em tempo de execução vs. alocação em tempo de compilação (globais inicializados) é o tamanho do arquivo hexadecimal. Quando os sistemas embarcados eram construídos com computadores prontos para uso com toda a memória volátil, o programa era frequentemente carregado no sistema incorporado a partir de uma rede ou de um computador de instrumentação, e o tempo de carregamento às vezes era um problema. Deixar buffers cheios de zeros da imagem pode reduzir o tempo consideravelmente.)

Se eu precisar de alocação dinâmica de memória em um sistema incorporado, geralmente malloc(), ou preferencialmente, aloco estaticamente, um pool grande e o divido em buffers de tamanho fixo (ou um pool de buffers pequenos e grandes, respectivamente) e faço minha própria alocação / desalocação desse pool. Cada solicitação de qualquer quantidade de memória até o tamanho fixo do buffer é atendida com um desses buffers. A função de chamada não precisa saber se é maior que a solicitada e, ao evitar a divisão e a combinação de blocos, resolvemos a fragmentação. Obviamente, vazamentos de memória ainda podem ocorrer se o programa alocar / desalocar bugs.


Outra nota histórica, isso rapidamente levou ao segmento BSS, que permitiu que um programa zere sua própria memória para inicialização, sem copiar lentamente os zeros durante o carregamento do programa.
rsaxvc

16

Normalmente, ao escrever esboços do Arduino, você evita a alocação dinâmica (seja com mallocou newpara instâncias C ++), as pessoas usam staticvariáveis globais -ou- , ou variáveis ​​locais (pilha).

O uso da alocação dinâmica pode levar a vários problemas:

  • vazamentos de memória (se você perder um ponteiro para uma memória que você alocou anteriormente, ou mais provavelmente se você esquecer de liberar a memória alocada quando não precisar mais dela)
  • fragmentação da heap (depois de vários malloc/ freeas chamadas), onde a pilha cresce thant a quantidade real de memória alocada atualmente

Na maioria das situações que enfrentei, a alocação dinâmica não era necessária ou poderia ser evitada com macros, como no seguinte exemplo de código:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

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

Sem #define BUFFER_SIZE, se quiséssemos que a Dummyclasse tivesse um buffertamanho não fixo , teríamos que usar a alocação dinâmica da seguinte maneira:

class Dummy
{
    const byte* buffer;

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

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

Nesse caso, temos mais opções do que no primeiro exemplo (por exemplo, use Dummyobjetos diferentes com buffertamanhos diferentes para cada um), mas podemos ter problemas de fragmentação de heap.

Observe o uso de um destruidor para garantir que a memória alocada dinamicamente bufferseja liberada quando uma Dummyinstância for excluída.


14

Dei uma olhada no algoritmo usado pelo malloc()avr-libc e parece haver alguns padrões de uso que são seguros do ponto de vista da fragmentação de heap:

1. Aloque apenas buffers de longa duração

Com isso, quero dizer: aloque tudo o que você precisa no início do programa e nunca o liberte. Obviamente, nesse caso, você também pode usar buffers estáticos ...

2. Aloque apenas buffers de curta duração

Significado: você libera o buffer antes de alocar qualquer outra coisa. Um exemplo razoável pode ser assim:

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

Se não houver malloc dentro do_whatever_with()ou se essa função liberar o que quer que seja alocado, você estará protegido contra fragmentação.

3. Sempre libere o último buffer alocado

Esta é uma generalização dos dois casos anteriores. Se você usar o heap como uma pilha (a última a entrar é a primeira a sair), ela se comportará como uma pilha e não como um fragmento. Deve-se notar que, nesse caso, é seguro redimensionar o último buffer alocado com realloc().

4. Aloque sempre o mesmo tamanho

Isso não impedirá a fragmentação, mas é seguro no sentido de que o heap não crescerá maior que o tamanho máximo usado . Se todos os seus buffers tiverem o mesmo tamanho, você pode ter certeza de que, sempre que liberar um deles, o slot estará disponível para alocações subsequentes.


11
O padrão 2 deve ser evitado, pois adiciona ciclos para malloc () e free () quando isso pode ser feito com "char buffer [size];" (em C ++). Eu também gostaria de adicionar o antipadrão "Nunca de um ISR".
Mikael Patel

9

O uso da alocação dinâmica (via malloc/ freeou new/ delete) não é inerentemente ruim como tal. De fato, para algo como o processamento de strings (por exemplo, através do Stringobjeto), geralmente é bastante útil. Isso ocorre porque muitos esboços usam vários pequenos fragmentos de cordas, que eventualmente se combinam em um maior. O uso da alocação dinâmica permite usar apenas a quantidade de memória necessária para cada uma. Por outro lado, o uso de um buffer estático de tamanho fixo para cada um pode acabar desperdiçando muito espaço (fazendo com que a memória fique muito mais rápida), embora dependa inteiramente do contexto.

Com tudo isso dito, é muito importante garantir que o uso da memória seja previsível. Permitir que o esboço use quantidades arbitrárias de memória, dependendo das circunstâncias do tempo de execução (por exemplo, entrada), pode facilmente causar um problema mais cedo ou mais tarde. Em alguns casos, pode ser perfeitamente seguro, por exemplo, se você souber que o uso nunca será demais. Os esboços podem mudar durante o processo de programação. Uma suposição feita desde o início pode ser esquecida quando algo é alterado posteriormente, resultando em um problema imprevisto.

Para maior robustez, geralmente é melhor trabalhar com buffers de tamanho fixo sempre que possível e projetar o esboço para trabalhar explicitamente com esses limites desde o início. Isso significa que quaisquer alterações futuras no esboço ou circunstâncias inesperadas em tempo de execução não devem causar problemas de memória.


6

Eu discordo de pessoas que pensam que você não deve usá-lo ou que geralmente é desnecessário. Acredito que pode ser perigoso se você não souber os detalhes, mas é útil. Eu tenho casos em que não sei (e não deveria saber) o tamanho de uma estrutura ou de um buffer (em tempo de compilação ou tempo de execução), especialmente quando se trata de bibliotecas que eu envio para o mundo. Concordo que, se o seu aplicativo estiver lidando apenas com uma única estrutura conhecida, você deve apenas usar esse tamanho no momento da compilação.

Exemplo: Eu tenho uma classe de pacote serial (uma biblioteca) que pode receber cargas de dados de tamanho arbitrário (pode ser struct, matriz de uint16_t, etc.). No final do envio dessa classe, basta informar ao método Packet.send () o endereço da coisa que você deseja enviar e a porta HardwareSerial através da qual deseja enviá-la. No entanto, no lado de recebimento, preciso de um buffer de recebimento alocado dinamicamente para manter essa carga útil recebida, pois essa carga útil pode ser uma estrutura diferente a qualquer momento, dependendo do estado do aplicativo, por exemplo. Se eu estiver enviando uma única estrutura para frente e para trás, eu faria o buffer do tamanho necessário para compilar. Mas, no caso em que os pacotes podem ter comprimentos diferentes ao longo do tempo, malloc () e free () não são tão ruins.

Executo testes com o código a seguir há dias, deixando-o fazer um loop contínuo e não encontrei evidências de fragmentação da memória. Após liberar a memória alocada dinamicamente, a quantidade livre retorna ao seu valor anterior.

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

Não vi nenhum tipo de degradação na RAM ou na minha capacidade de alocá-la dinamicamente usando esse método, por isso diria que é uma ferramenta viável. FWIW.


2
Seu código de teste está em conformidade com o padrão de uso 2. Aloque apenas buffers de vida curta que descrevi na minha resposta anterior. Esse é um daqueles poucos padrões de uso conhecidos por serem seguros.
Edgar Bonet

Em outras palavras, os problemas surgirão quando você começar a compartilhar o processador com outro código desconhecido - que é precisamente o problema que você pensa que está evitando. Geralmente, se você deseja algo que sempre funcione ou falhe durante a vinculação, faça uma alocação fixa do tamanho máximo e use-a repetidamente, por exemplo, fazendo com que seu usuário o repasse na inicialização. Lembre-se de que você normalmente está rodando em um chip no qual tudo precisa caber em 2048 bytes - talvez mais em algumas placas, mas talvez muito menos em outras.
Chris Stratton

@EdgarBonet Sim, exatamente. Só queria compartilhar.
StuffAndyMakes

11
A alocação dinâmica de um buffer apenas do tamanho necessário é arriscada, como se qualquer outra coisa fosse alocada antes da liberação, você pode ficar com fragmentação - memória que não pode ser reutilizada. Além disso, a alocação dinâmica possui sobrecarga de rastreamento. A alocação fixa não significa que você não pode multiplicar o uso da memória, apenas significa que você precisa trabalhar o compartilhamento no design do seu programa. Para um buffer com escopo puramente local, você também pode avaliar o uso da pilha. Você também não verificou a possibilidade de malloc () falhar.
Chris Stratton

11
"pode ​​ser perigoso se você não souber os detalhes, mas é útil." resume praticamente todo o desenvolvimento em C / C ++. :-)
ThatAintWorking

4

É uma péssima idéia usar malloc () e free () com o Arduino?

A resposta curta é sim. Abaixo estão as razões pelas quais:

Trata-se de entender o que é uma MPU e como programar dentro das restrições dos recursos disponíveis. O Arduino Uno usa um MPU ATmega328p com memória flash ISP de 32KB, EEPROM 1024B e SRAM de 2KB. Isso não é muitos recursos de memória.

Lembre-se de que a SRAM de 2 KB é usada para todas as variáveis ​​globais, literais de string, pilha e possível uso do heap. A pilha também precisa ter espaço para um ISR.

O layout da memória é:

Mapa SRAM

Atualmente, os PC / laptops têm mais de 1.000.000 de vezes a quantidade de memória. Um espaço de pilha padrão de 1 Mbyte por thread não é incomum, mas é totalmente irreal em uma MPU.

Um projeto de software incorporado precisa fazer um orçamento de recursos. Isso está estimando a latência do ISR, o espaço necessário na memória, a energia da computação, os ciclos de instruções etc. Infelizmente, infelizmente, não há almoços grátis e programação embutida em tempo real é a habilidade de programação mais difícil de dominar.


Amém: "A programação embutida em tempo real é a mais difícil das habilidades de programação para dominar."
StuffAndyMakes

O tempo de execução do malloc é sempre o mesmo? Eu posso imaginar o malloc levando mais tempo, à medida que procura mais na memória RAM disponível por um slot que se encaixa? Este seria outro argumento (além de ficar sem memória RAM) para não alocar memória em movimento?
Paul

@Paul Os algoritmos de heap (malloc e free) normalmente não são tempo de execução constante e não são reentrantes. O algoritmo contém estruturas de pesquisa e dados que requerem bloqueios ao usar threads (simultaneidade).
Mikael Patel

0

Ok, eu sei que essa é uma pergunta antiga, mas quanto mais eu leio as respostas, mais eu continuo voltando a uma observação que parece saliente.

O problema da parada é real

Parece haver um link para o problema de parada de Turing aqui. Permitir alocação dinâmica aumenta as chances de dita "parada", de modo que a questão se torna de tolerância a riscos. Embora seja conveniente renunciar à possibilidade de malloc()falha e assim por diante, ainda é um resultado válido. A pergunta feita pelo OP parece ser apenas técnica, e sim os detalhes das bibliotecas usadas ou da MPU específica são importantes; a conversa se volta para reduzir o risco de interrupção do programa ou qualquer outro fim anormal. Precisamos reconhecer a existência de ambientes que toleram riscos muito diferentes. Meu projeto de hobby de exibir cores bonitas em uma faixa de LED não matará alguém se algo incomum acontecer, mas o MCU dentro de uma máquina de pulmão cardíaco provavelmente matará.

Olá Sr. Turing Meu nome é Hubris

Para a minha faixa de LED, não me importo se ela trava, vou apenas redefini-la. Se eu estivesse em uma máquina de coração-pulmão controlada por um MCU, as consequências de travar ou deixar de operar são literalmente vida ou morte, então a questão malloc()e free()deveria ser dividida entre como o programa pretendido lida com a possibilidade de demonstrar o Sr. O famoso problema de Turing. Pode ser fácil esquecer que é uma prova matemática e convencer-nos de que, se formos suficientemente espertos, podemos evitar ser uma vítima dos limites da computação.

Esta pergunta deve ter duas respostas aceitas, uma para aqueles que são forçados a piscar quando encaram O problema da parada e uma para todas as outras. Embora a maioria dos usos do arduino provavelmente não seja de missão crítica ou de vida ou morte, a distinção ainda existe, independentemente de qual MPU você esteja codificando.


Eu não acho que o problema da parada se aplique nessa situação específica, considerando o fato de que o uso de heap não é necessariamente arbitrário. Se usado de maneira bem definida, o uso de heap se torna previsivelmente "seguro". O ponto do problema da parada foi descobrir se é possível determinar o que acontece com um algoritmo necessariamente arbitrário e não tão bem definido. Ele realmente se aplica muito mais à programação em um sentido mais amplo e, como tal, acho que não é especificamente relevante aqui. Eu nem acho que é relevante ser totalmente honesto.
Jonathan Gray

Admito algum exagero retórico, mas o ponto é realmente se você deseja garantir o comportamento, usar o heap implica um nível de risco muito maior do que continuar usando apenas a pilha.
Kelly S. French

-3

Não, mas eles devem ser usados ​​com muito cuidado em relação à liberação () de memória alocada. Eu nunca entendi por que as pessoas dizem que o gerenciamento direto de memória deve ser evitado, pois implica um nível de incompetência que geralmente é incompatível com o desenvolvimento de software.

Vamos dizer que você está usando seu arduino para controlar um drone. Qualquer erro em qualquer parte do seu código pode fazer com que ele caia do céu e machuque alguém ou algo. Em outras palavras, se alguém não possui as competências necessárias para usar o malloc, provavelmente não deve codificar, pois existem muitas outras áreas em que pequenos erros podem causar problemas sérios.

Os bugs causados ​​pelo malloc são mais difíceis de localizar e corrigir? Sim, mas isso é mais uma questão de frustração por parte dos codificadores do que de risco. Quanto ao risco, qualquer parte do seu código pode ser igualmente ou mais arriscada do que o malloc, se você não tomar as medidas necessárias para garantir que tudo seja feito corretamente.


4
É interessante você ter usado um drone como exemplo. De acordo com este artigo ( mil-embedded.com/articles/… ), "Devido ao seu risco, a alocação dinâmica de memória é proibida, de acordo com o padrão DO-178B, no código aviônico incorporado de segurança crítica".
Gabriel Staples

A DARPA tem um longo histórico de permitir que os contratados desenvolvam especificações que se encaixam em sua própria plataforma - por que não deveriam quando os contribuintes pagam a conta? É por isso que custa US $ 10 bilhões para eles desenvolverem o que os outros podem fazer com US $ 10.000. Quase parece que você está usando o complexo industrial militar como uma referência honesta.
JSON

A alocação dinâmica parece um convite para o seu programa demonstrar os limites de computação descritos no Problema da parada. Existem alguns ambientes que podem lidar com uma pequena quantidade de risco de tal interrupção e existem ambientes (espaço, defesa, medicina, etc.) que não toleram qualquer quantidade de risco controlável; portanto, eles não permitem operações que "não deveriam" falha porque 'deve funcionar' não é bom o suficiente quando você está lançando um foguete ou controlando uma máquina de coração / pulmão.
Kelly S. French
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.