Como espremer código para mais Flash e RAM? [fechadas]
Eu trabalhei no desenvolvimento de um recurso em um produto específico. Houve um pedido para portar o mesmo recurso para outro produto. Este produto é baseado em um microcontrolador M16C, que tradicionalmente possui 64K Flash e 2k de RAM.
É um produto maduro e, portanto, possui apenas 132 bytes de Flash e 2 bytes de RAM restantes.
Para portar o recurso solicitado (o próprio recurso foi otimizado), preciso de 1400 bytes de Flash e ~ 200 bytes de RAM.
Alguém tem alguma sugestão sobre como recuperar esses bytes por compactação de código? Que coisas específicas procuro quando estou tentando compactar o código de trabalho já existente?
Todas as idéias serão realmente apreciadas.
— IntelliChick
Obrigado a todos pelas sugestões. Vou mantê-lo atualizado com meu progresso e listar as etapas que funcionaram e as que não funcionaram.
Ok, então aqui estão as coisas que tentei que funcionaram: Movido as versões do compilador. A otimização melhorou drasticamente, o que me proporcionou aproximadamente 2K de Flash. Percorreu os arquivos da lista para verificar a funcionalidade redundante e não utilizada (herdada por causa da base de código comum) para o produto em particular e ganhou um pouco mais de Flash.
Para a RAM, fiz o seguinte: Passei pelo arquivo de mapa para verificar as funções / módulos que estavam usando mais RAM. Encontrei uma função realmente pesada (12 canais, cada um com uma quantidade fixa de memória alocada), de código legado, entendi o que estava tentando alcançar e otimizei o uso da RAM, compartilhando informações entre os canais comuns. Isso me deu ~ 200 bytes que eu precisava.
Se você tiver arquivos ascii, poderá usar a compressão de 8 a 7 bits. Economiza 12,5%. Usar um arquivo zip exigiria mais código para compactar e descompactá-lo do que apenas deixá-lo.
Você tem algumas opções: primeiro é procurar código redundante e movê-lo para uma única chamada para se livrar da duplicação; o segundo é remover a funcionalidade.
Dê uma boa olhada no seu arquivo .map e veja se há funções das quais você pode se livrar ou reescrever. Verifique também se as chamadas de biblioteca que estão sendo usadas são realmente necessárias.
Certas coisas, como divisão e multiplicações, podem trazer muito código, mas o uso de turnos e um melhor uso de constantes podem diminuir o código. Também dê uma olhada em coisas como constantes de string printfes. Por exemplo, cada printfum consumirá sua rom, mas você poderá ter algumas seqüências de caracteres de formato compartilhado, em vez de repetir essa sequência constantemente uma e outra vez.
Para a memória, veja se você pode se livrar de globais e usar automóveis em uma função. Evite também o maior número possível de variáveis ​​na função principal, pois elas consomem memória da mesma forma que os globais.
Obrigado pelas sugestões, posso definitivamente tentar a maioria delas, exceto a constante de string. É puramente um dispositivo incorporado, sem interface do usuário e, portanto, não há chamadas para printf () dentro do código. Na esperança de que essas sugestões me ajudem a obter 1400 bytes de Flash / 200 bytes de RAM que eu preciso.
@IntelliChick, você ficaria surpreso com quantas pessoas usam printf () dentro de um dispositivo incorporado para imprimir para depuração ou envio para um periférico. Parece que você sabe melhor do que isso, mas se alguém tiver escrito código no projeto antes de você, não faria mal verificar isso.
E apenas para expandir meu comentário anterior, você também ficaria surpreso com quantas pessoas adicionam instruções de depuração, mas nunca as remove. Até as pessoas que fazem #ifdefs ainda ficam preguiçosas às vezes.
Impressionante, obrigado! Eu herdei essa base de código, então mal vou dar uma olhada nelas. Vou manter vocês informados sobre o progresso e tentar acompanhar quantos bytes de memória ou Flash ganhei fazendo o que, apenas como referência para qualquer pessoa que precise fazer isso no futuro.
Apenas uma pergunta sobre isso - que tal a chamada de função aninhada saltar de uma camada para outra. Quanta sobrecarga isso acrescenta? É melhor manter a modularidade tendo várias chamadas de função ou reduzindo as chamadas de função e economizando alguns bytes. E isso é significativo?
Sempre vale a pena examinar a saída do arquivo de listagem (assembler) para procurar coisas nas quais seu compilador é particularmente ruim.
Por exemplo, você pode achar que as variáveis ​​locais são muito caras e, se o aplicativo é simples o suficiente para valer o risco, mover alguns contadores de loop para variáveis ​​estáticas pode economizar muito código.
Ou a indexação de matriz pode ser muito cara, mas as operações com ponteiros são muito mais baratas. Ou vice-versa.
Mas olhar para a linguagem assembly é o primeiro passo.
É muito importante que você saiba o que seu compilador faz. Você deve ver o que está dividido no meu compilador. Faz os bebês chorarem (inclusive eu).
As otimizações do compilador, por exemplo, -Osno GCC oferecem o melhor equilíbrio entre velocidade e tamanho do código. Evite -O3, pois isso pode aumentar o tamanho do código.
Se você fizer isso, precisará testar novamente TUDO! As otimizações podem fazer com que o código ativo não funcione devido a novas suposições feitas pelo compilador.
— 8407 Robert
@ Robert, isso só é verdade se você usar instruções indefinidas: por exemplo, a = a ++ compilará diferentemente em -O0 e -O3.
@ Thomas não é verdade. Se você tiver um loop for para atrasar os ciclos do relógio, muitos otimizadores perceberão que você não está fazendo nada nele e o removerão. Este é apenas um exemplo.
@ Thomas O, Você também precisa ter cuidado com as definições de funções voláteis. Os otimizadores explodem aqueles que pensam que conhecem bem C, mas não entendem as complexidades das operações atômicas.
Todos os bons pontos. Funções / variáveis ​​voláteis, por definição, NÃO devem ser otimizadas. Qualquer otimizador que realize otimizações em tais (incluindo tempo de chamada e inlining) está quebrado.
Para RAM, verifique o intervalo de todas as suas variáveis ​​- você está usando ints onde poderia usar um caractere? Os buffers são maiores do que precisam?
A compressão de código depende muito do aplicativo e do estilo de codificação. Seus valores restantes sugerem que talvez o código já tenha passado por alguns comprimentos, o que pode significar que resta pouco.
Também dê uma olhada na funcionalidade geral - existe algo que não é realmente usado e pode ser descartado?
Se é um projeto antigo, mas o compilador foi desenvolvido desde então, pode ser que um compilador mais recente produza um código menor
Obrigado Mike! Eu tentei isso no passado, e o espaço ganho já foi usado! :) Movido do compilador IAR C 3.21d para 3.40.
Mudei para mais uma versão e consegui colocar um pouco mais de Flash para ajustar o recurso. No entanto, estou realmente lutando com a RAM, que permaneceu inalterada. :(
Sempre vale a pena conferir o manual do compilador em busca de opções para otimizar o espaço.
Para o gcc -ffunction-sectionse -fdata-sectionscom o --gc-sectionssinalizador do linker, é bom remover o código morto.
Aqui estão algumas outras dicas excelentes (voltadas para o AVR)
Isso realmente funciona? Os documentos dizem "Quando você especifica essas opções, o assembler e o vinculador criarão objetos maiores e arquivos executáveis ​​e também serão mais lentos". Entendo que ter seções separadas faz sentido para um micro com seções Flash e RAM - esta declaração nos documentos não é aplicável aos microcontroladores?
Minha experiência é que ele funciona bem para AVR
Isso não funciona bem na maioria dos compiladores que usei. É como usar a palavra-chave register. Você pode dizer ao compilador que uma variável entra em um registro, mas um bom otimizador fará isso muito melhor do que um humano (argumenta que alguns podem, não é aceitável fazer isso na prática).
Quando você começa a atribuir locais, você está forçando o compilador a colocar coisas em determinados locais, muito importantes para o código do gerenciador de inicialização avançado, mas difíceis de lidar no otimizador, pois ao tomar decisões, você está dando um passo na otimização. poderia fazer. Em alguns compiladores, eles o projetam para ter seções para qual código é usado; este é um caso de informar ao compilador mais informações para entender seu uso, isso ajudará. Se o compilador não sugerir, não faça.
Você pode examinar a quantidade de espaço de pilha e de pilha que estão alocados. Você poderá recuperar uma quantidade substancial de RAM se um ou ambos estiverem com excesso de alocação.
Meu palpite é que, para começar um projeto que se encaixa em 2k de RAM, não há alocação dinâmica de memória (o uso de malloc , calloc, etc.). Se esse for o caso, você pode se livrar completamente do heap, assumindo que o autor original tenha deixado alguma RAM alocada para o heap.
Você precisa ter muito cuidado ao reduzir o tamanho da pilha, pois isso pode causar bugs que são muito difíceis de encontrar. Pode ser útil começar inicializando todo o espaço da pilha para um valor conhecido (algo diferente de 0x00 ou 0xff, pois esses valores já ocorrem normalmente) e, em seguida, execute o sistema por um tempo para ver quanto espaço da pilha não está sendo usado.
Essas são escolhas muito boas. Observarei que você nunca deve usar o malloc em um sistema incorporado.
@Kortuk Isso depende de sua definição de embutido e a tarefa executada
@joby, Sim, eu entendo isso. Em um sistema com 0 reinicialização e a ausência de um sistema operacional como o linux, o Malloc pode ser muito, muito ruim.
Não há alocação dinâmica de memória, nenhum lugar onde malloc, calloc está sendo usado. Também verifiquei a alocação de heap, e ela já foi definida como 0, portanto, não há alocação de heap. O tamanho da pilha alocado atualmente é 254 bytes e o tamanho da pilha de interrupção em 128 bytes.
Seu código usa matemática de ponto flutuante? Você poderá reimplementar seus algoritmos usando apenas matemática inteira e eliminar as despesas gerais do uso da biblioteca de ponto flutuante C. Por exemplo, em algumas aplicações, funções como sine, log, exp podem ser substituídas por aproximações polinomiais inteiras.
Seu código usa grandes tabelas de pesquisa para algoritmos, como cálculos de CRC? Você pode tentar substituir uma versão diferente do algoritmo que calcula valores rapidamente, em vez de usar as tabelas de consulta. A ressalva é que o algoritmo menor provavelmente é mais lento, portanto, verifique se você tem ciclos de CPU suficientes.
Seu código possui grandes quantidades de dados constantes, como tabelas de string, páginas HTML ou gráficos de pixel (ícones)? Se for grande o suficiente (digamos 10 kB), pode valer a pena implementar um esquema de compactação muito simples para reduzir os dados e descompactá-los quando necessário.
Existem 2 pequenas tabelas de pesquisa, nenhuma das quais equivalerá a 10K, infelizmente. E nenhuma matemática de ponto flutuante também está sendo usada. :( Obrigado pelas sugestões. Elas são boas.
Você pode tentar reorganizar para codificar muito, para um estilo mais compacto. Depende muito do que o código está fazendo. A chave é encontrar coisas semelhantes e reimplementá-las em termos uma da outra. Um extremo seria usar uma linguagem de nível superior, como a Forth, com a qual pode ser mais fácil obter uma densidade de código mais alta do que em C ou assembler.
Aqui está a seguir para M16C .
— Falken apoia Monica
Defina o nível de otimização do compilador. Muitos IDEs têm configurações que permitem otimizações de tamanho de código às custas do tempo de compilação (ou talvez até o tempo de processamento em alguns casos). Eles podem realizar a compactação de código reexecutando o otimizador algumas vezes, procurando padrões menos otimizáveis ​​e uma série de truques que podem não ser necessários para a compilação casual / depuração. Normalmente, por padrão, os compiladores são configurados para um nível médio de otimização. Navegue pelas configurações e encontre uma escala de otimização baseada em números inteiros.
Atualmente otimizado para o máximo de tamanho. :) Obrigado pela sugestão embora. :)
Se você já estiver usando um compilador de nível profissional como o IAR, acho que você terá dificuldades para obter economias sérias com pequenos ajustes de código de baixo nível - precisará procurar mais para remover a funcionalidade ou realizar grandes reescreve as peças de maneira mais eficiente. Você precisará ser um codificador mais inteligente do que quem escreveu a versão original ... Quanto à RAM, você precisa examinar muito bem como é usada atualmente e verificar se há espaço para sobrepor o uso da mesma RAM para coisas diferentes em momentos diferentes (os sindicatos são úteis para isso). Os tamanhos padrão de pilha e pilha do IAR nos modelos ARM / AVR que eu costumava ser excessivamente generosos, portanto, essa seria a primeira coisa a considerar.
Obrigado Mike. O código já está usando uniões na maioria dos lugares, mas vou dar uma olhada em outros lugares, onde isso ainda pode ajudar. Também vou dar uma olhada no tamanho da pilha escolhido e ver se isso pode ser otimizado.
Como sei que tamanho de tamanho de pilha é apropriado?
Alguma outra coisa para verificar - alguns compiladores em algumas arquiteturas copiam constantes para a RAM - normalmente usados ​​quando o acesso a constantes flash é lento / difícil (por exemplo, AVR), por exemplo, o compilador AVR do IAR exige que um qualificador _ _flash não copie uma constante para a RAM)
Obrigado Mike. Sim, eu já tinha verificado isso - é chamada a opção 'Constantes graváveis' para o compilador M16C IAR C. Ele copia as constantes da ROM para a RAM. Esta opção está desmarcada para o meu projeto. Mas uma verificação realmente válida! Obrigado.
Se o seu processador não tiver suporte de hardware para uma pilha de parâmetro / local, mas o compilador tentar implementar uma pilha de parâmetros em tempo de execução de qualquer maneira, e se seu código não precisar ser reentrante, você poderá salvar o código espaço alocando estaticamente variáveis ​​automáticas. Em alguns casos, isso deve ser feito manualmente; em outros casos, as diretivas do compilador podem fazê-lo. A alocação manual eficiente exigirá o compartilhamento de variáveis ​​entre as rotinas. Esse compartilhamento deve ser feito com cuidado, para garantir que nenhuma rotina use uma variável que outra rotina considere "dentro do escopo", mas, em alguns casos, os benefícios do tamanho do código podem ser significativos.
Alguns processadores possuem convenções de chamada que podem tornar alguns estilos de passagem de parâmetro mais eficientes que outros. Por exemplo, nos controladores PIC18, se uma rotina usa um único parâmetro de um byte, ela pode ser passada em um registro; se for necessário mais do que isso, todos os parâmetros devem ser passados ​​na RAM. Se uma rotina usar dois parâmetros de um byte, pode ser mais eficiente "passar" um em uma variável global e depois passar o outro como parâmetro. Com rotinas amplamente usadas, a economia pode aumentar. Eles podem ser especialmente significativos se o parâmetro passado via global for um sinalizador de bit único ou se normalmente terá um valor de 0 ou 255 (já que existem instruções especiais para armazenar um 0 ou 255 na RAM).
No ARM, colocar variáveis ​​globais que são frequentemente usadas juntas em uma estrutura pode reduzir significativamente o tamanho do código e melhorar o desempenho. Se A, B, C, D e E são variáveis ​​globais separadas, o código que usa todos eles deve carregar o endereço de cada um em um registro; se não houver registros suficientes, pode ser necessário recarregar esses endereços várias vezes. Por outro lado, se eles fazem parte da mesma estrutura global MyStuff, o código que usa MyStuff.A, MyStuff.B etc. pode simplesmente carregar o endereço do MyStuff uma vez. Grande vitória.
1.Se o seu código depende de muitas estruturas, verifique se os membros da estrutura estão ordenados entre aqueles que ocupam mais memória e menos.
Ex: "uint32_t uint16_t uint8_t" em vez de "uint16_t uint8_t uint32_t"
Isso garantirá o preenchimento mínimo da estrutura.
2.Use const para variáveis, quando aplicável. Isso garantirá que essas variáveis ​​estejam na ROM e não consumam RAM
— AkshayImmanuelD
Alguns truques (talvez óbvios) que usei com êxito para compactar o código de algum cliente:
Compactar sinalizadores em campos de bits ou máscaras de bits. Isso pode ser benéfico, pois geralmente os booleanos são armazenados como números inteiros, desperdiçando memória. Isso economizará RAM e ROM e geralmente não é feito pelo compilador.
Procure redundância no código e use loops ou funções para executar instruções repetidas.
Também salvei alguma ROM substituindo muitas if(x==enum_entry) <assignment>instruções de constantes por uma matriz indexada, cuidando para que as entradas enum pudessem ser usadas como o índice da matriz
Se possível, use funções embutidas ou macros do compilador em vez de pequenas funções. Há sobrecarga de tamanho e velocidade com a passagem de argumentos e outros que podem ser remediados com a função embutida.
Qualquer compilador decente deve fazer isso automaticamente para funções chamadas apenas uma vez.
Descobri que inlining geralmente é mais útil para otimizações de velocidade e geralmente ao custo de um tamanho maior.
inlining irá tipicamente aumentar o tamanho do código, exceto com funções triviais comoint get_a(struct x) {return x.a;}
Mude as variáveis ​​locais para o mesmo tamanho dos seus registros de CPU.
Se a CPU for de 32 bits, use variáveis ​​de 32 bits, mesmo que o valor máximo nunca fique acima de 255. Se você usou uma variável de 8 bits, o compilador adicionará código para mascarar os 24 bits superiores.
O primeiro lugar que eu procuraria é nas variáveis ​​de loop for.
Pode parecer um bom lugar para uma variável de 8 bits, mas uma variável de 32 bits pode produzir menos código.
Isso pode economizar código, mas consumirá RAM.
Ele só consumirá RAM se essa chamada de função estiver no ramo mais longo do rastreamento de chamada. Caso contrário, é reutilizar o espaço de pilha que alguma outra função já precisa.
Geralmente verdadeiro, desde que seja uma variável local. Se houver pouca RAM, o tamanho dos vars globais, especialmente matrizes, é um bom lugar para começar a procurar economia.
— Mikeelectricstuff
Outra possibilidade, curiosamente, é substituir variáveis ​​não assinadas por assinadas. Se um compilador otimizar um curto não assinado para um registro de 32 bits, ele deverá adicionar código para garantir que seu valor mude de 65535 para zero. Se, no entanto, o compilador otimizar um atalho assinado para um registro, esse código não será necessário. Como não há garantia do que acontecerá se um curto for incrementado além de 32767, os compiladores não precisam emitir código para lidar com isso. Em pelo menos dois compiladores ARM que eu observei, o código abreviado assinado pode ser menor que o código abreviado não assinado por esse motivo.