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?
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?
Respostas:
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.
Normalmente, ao escrever esboços do Arduino, você evita a alocação dinâmica (seja com malloc
ou new
para instâncias C ++), as pessoas usam static
variáveis globais -ou- , ou variáveis locais (pilha).
O uso da alocação dinâmica pode levar a vários problemas:
malloc
/ free
as chamadas), onde a pilha cresce thant a quantidade real de memória alocada atualmenteNa 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 Dummy
classe tivesse um buffer
tamanho 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 Dummy
objetos diferentes com buffer
tamanhos 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 buffer
seja liberada quando uma Dummy
instância for excluída.
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:
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 ...
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.
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()
.
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.
O uso da alocação dinâmica (via malloc
/ free
ou new
/ delete
) não é inerentemente ruim como tal. De fato, para algo como o processamento de strings (por exemplo, através do String
objeto), 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.
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.
É 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 é:
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.
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.
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á.
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.
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.