Quali sono le migliori pratiche per ridurre l’utilizzo della memoria in C?

Quali sono le migliori pratiche per “Programmazione C efficiente in memoria”. Principalmente per dispositivi embedded / mobili quali dovrebbero essere le linee guida per avere bassi consumi di memoria?

Suppongo che ci dovrebbe essere una linea guida separata per a) memoria codice b) memoria dati

In C, ad un livello molto più semplice, considera quanto segue;

  • Usa #pragma pack (1) per allineare le tue strutture
  • Utilizza i sindacati in cui una struttura può contenere diversi tipi di dati
  • Utilizzare i campi di bit anziché gli interi per archiviare i flag e i piccoli numeri interi
  • Evitare l’uso di array di caratteri a lunghezza fissa per memorizzare stringhe, implementare un pool di stringhe e utilizzare i puntatori.
  • Dove si memorizzano i riferimenti a una lista di stringhe numerata, ad esempio il nome del font, si memorizza un indice nella lista piuttosto che la stringa
  • Quando si utilizza l’allocazione dynamic della memoria, calcolare il numero di elementi richiesti in anticipo per evitare riallocamenti.

Alcuni suggerimenti che ho trovato utili nel lavorare con i sistemi embedded:

  • Assicurarsi che qualsiasi tabella di ricerca o altri dati costanti siano effettivamente dichiarati utilizzando const . Se si utilizza const i dati possono essere memorizzati in sola lettura (ad es., Flash o EEPROM), altrimenti i dati devono essere copiati nella RAM all’avvio e questo richiede sia il flash che lo spazio RAM. Imposta le opzioni del linker in modo che generi un file di mappa e studi questo file per vedere esattamente dove vengono allocati i tuoi dati nella mappa della memoria.

  • Assicurati di utilizzare tutte le aree di memoria disponibili. Ad esempio, i microcontrollori hanno spesso memoria integrata che è ansible utilizzare (che può anche essere più veloce per accedere rispetto alla RAM esterna). Dovresti essere in grado di controllare le regioni di memoria a cui sono allocati il ​​codice e i dati utilizzando le impostazioni dell’opzione del compilatore e del linker.

  • Per ridurre le dimensioni del codice, controlla le impostazioni di ottimizzazione del compilatore. La maggior parte dei compilatori ha interruttori per ottimizzare la velocità o la dimensione del codice. Può valere la pena di sperimentare con queste opzioni per vedere se la dimensione del codice compilato può essere ridotta. E ovviamente, elimina il codice duplicato ovunque sia ansible.

  • Controlla la quantità di memoria dello stack necessaria per il tuo sistema e regola di conseguenza l’allocazione della memoria del linker (vedi le risposte a questa domanda ). Per ridurre l’utilizzo dello stack, evitare di collocare strutture di dati di grandi dimensioni nello stack (per qualsiasi valore di “large” è rilevante per te).

Assicurati di utilizzare matematica a punti fissi / interi laddove ansible. Molti sviluppatori usano la matematica a virgola mobile (insieme alle prestazioni lente e alle librerie di grandi dimensioni e all’utilizzo della memoria) quando sarà sufficiente una matematica integer con scala semplice.

Tutti buoni consigli. Ecco alcuni approcci progettuali che ho trovato utili.

  • Codifica dei byte

Scrivi un interprete per un set di istruzioni byte-code per scopi speciali e scrivi il maggior numero ansible di programmi in quel set di istruzioni. Se determinate operazioni richiedono prestazioni elevate, renderle native-code e chiamarle dall’interprete.

  • Generazione del codice

Se una parte dei dati di input cambia molto di rado, è ansible avere un generatore di codice esterno che crea un programma ad hoc. Sarà più piccolo di un programma più generale, oltre a essere più veloce e non dover allocare spazio per l’input che cambia di rado.

  • Sii un odiatore di dati

Sii disposto a sprecare un sacco di cicli se ti consente di archiviare una struttura di dati assolutamente minima. Di solito scoprirai che le prestazioni soffrono pochissimo.

Evita la frammentazione della memoria usando il tuo allocatore di memoria (o usando attentamente l’allocatore di sistema).

Un metodo consiste nell’utilizzare un ‘allocatore di slab’ (vedere questo articolo per esempio) e più pool di memoria per oggetti di dimensioni diverse.

La pre-allocazione anticipata di tutta la memoria (ovvero nessuna chiamata di malloc tranne l’inizializzazione all’avvio) è decisamente utile per l’utilizzo della memoria deterministica. Altrimenti, diverse architetture forniscono tecniche per aiutare. Ad esempio, alcuni processori ARM forniscono un set di istruzioni alternativo (Thumb) che quasi dimezza le dimensioni del codice utilizzando le istruzioni a 16 bit anziché i normali 32 bit. Certo, la velocità è sacrificata nel farlo …

Molto probabilmente dovrai scegliere con cura i tuoi algoritmi. Mira agli algoritmi che hanno O (1) o O (log n) utilizzo della memoria (cioè basso). Ad esempio, gli array continui ridimensionabili (ad es. std::vector ) nella maggior parte dei casi richiedono meno memoria rispetto agli elenchi collegati.

A volte, l’utilizzo di tabelle di ricerca può essere più vantaggioso sia per le dimensioni del codice che per la velocità. Se hai solo 64 voci in una LUT, sono 16 * 4 byte per sin / cos / tan (usa simmetria!) Rispetto a una grande funzione sin / cos / tan.

La compressione a volte aiuta. Semplici algoritmi come RLE sono facili da comprimere / decomprimere quando letti in sequenza.

Se hai a che fare con grafica o audio, prendi in considerazione diversi formati. La grafica con palette o bitpacked * può essere un buon compromesso in termini di qualità e le tavolozze possono essere condivise su molte immagini, riducendo ulteriormente la dimensione dei dati. L’audio può essere ridotto da 16 bit a 8 bit o persino a 4 bit e lo stereo può essere convertito in mono. Le velocità di campionamento possono essere ridotte da 44.1KHz a 22kHz o 11kHz. Queste trasformazioni audio riducono notevolmente le dimensioni dei dati (e, purtroppo, la qualità) e sono banali (tranne il ricampionamento, ma questo è il software audio per =]).

* Immagino che potresti metterlo sotto compressione. Il bitpacking per la grafica di solito si riferisce alla riduzione del numero di bit per canale in modo che ciascun pixel possa essere contenuto in due byte (RGB565 o ARGB155 per esempio) o uno (ARGB232 o RGB332) dai tre o quattro originali (RGB888 o ARGB8888, rispettivamente).

Alcune operazioni di analisi possono essere eseguite sugli stream quando arrivano i byte anziché copiare nel buffer e analizzare.

Alcuni esempi di questo:

  1. Analizza un vapore NMEA con una macchina a stati, raccogliendo solo i campi richiesti in una struttura molto più efficiente.
  2. Analizza XML usando SAX invece di DOM.

1) Prima di iniziare il progetto, costruisci un metodo per misurare la quantità di memoria che stai utilizzando, preferibilmente su base per componente. In questo modo, ogni volta che apporti una modifica, puoi vedere i suoi effetti sull’uso della memoria. Non puoi ottimizzare ciò che non puoi misurare.

2) Se il progetto è già maturo e raggiunge i limiti di memoria, (o viene trasferito su un dispositivo con meno memoria), scopri per quale motivo stai già utilizzando la memoria.

La mia esperienza è stata che quasi tutta l’ottimizzazione significativa quando si aggiusta un’applicazione sovradimensionata deriva da un piccolo numero di modifiche: ridurre le dimensioni della cache, eliminare alcune trame (ovviamente si tratta di un cambiamento funzionale che richiede il consenso delle parti interessate, ad esempio le riunioni, quindi non essere efficiente in termini di tempo), ricampionare audio, ridurre le dimensioni iniziali degli heap allocati su misura, trovare modi per liberare risorse che vengono utilizzate solo temporaneamente e ricaricarle quando richiesto di nuovo. Occasionalmente troverai una struttura che è 64 byte che potrebbe essere ridotta a 16, o qualsiasi altra cosa, ma raramente è il frutto che pesa più in basso. Se sai quali sono le liste e gli array più grandi nell’app, allora sai quali sono le strutture da considerare per prime.

Oh sì: trova e risolvi le perdite di memoria. Ogni ricordo che puoi recuperare senza sacrificare le prestazioni è un ottimo inizio.

Ho passato molto tempo in passato a preoccuparmi delle dimensioni del codice. Principali considerazioni (a parte: assicurati di misurarlo in fase di compilazione in modo che tu possa vederlo cambiare), sono:

1) Scopri a che codice si fa riferimento e con cosa. Se scopri che un’intera libreria XML è collegata alla tua app solo per analizzare un file di configurazione a due elementi, valuta la possibilità di modificare il formato del file di configurazione e / o di scrivere il tuo parser banale. Se è ansible, utilizzare l’analisi di origine o binaria per disegnare un grafico a grande dipendenza e cercare componenti di grandi dimensioni con un numero limitato di utenti: potrebbe essere ansible eliminarli con solo riscritture di codice minori. Preparati a giocare a un diplomatico: se due componenti diversi nella tua app usano XML e vuoi tagliarli, allora sono due le persone che devi convincere dei benefici del rotolare a mano qualcosa che al momento è una libreria affidabile e immediatamente disponibile .

2) Scambia le opzioni del compilatore. Consultare la documentazione specifica della piattaforma. Ad esempio, è ansible ridurre l’aumento di dimensioni del codice accettabile per impostazione predefinita, e in GCC almeno si può dire al compilatore solo di applicare ottimizzazioni che in genere non aumentano le dimensioni del codice.

3) Approfitta delle librerie già sulle piattaforms di destinazione dove ansible, anche se ciò significa scrivere un livello adattatore. Nell’esempio XML sopra riportato, potresti scoprire che sulla tua piattaforma di destinazione c’è sempre una libreria XML in memoria, in ogni caso, perché il sistema operativo lo usa, nel qual caso collegalo dynamicmente.

4) Come menzionato da qualcun altro, la modalità pollice può aiutare su ARM. Se lo usi solo per il codice che non è critico per le prestazioni e lascia le routine critiche in ARM, non noterai la differenza.

Infine, ci possono essere trucchi intelligenti che puoi giocare se hai un controllo sufficiente sul dispositivo. L’interfaccia utente consente di eseguire una sola applicazione alla volta? Scarica tutti i driver e i servizi di cui l’app non ha bisogno. Lo schermo ha un doppio buffer, ma la tua app è sincronizzata con il ciclo di aggiornamento? Potrebbe essere ansible recuperare un intero buffer dello schermo.

Consiglia questo libro Small Memory Software: Patterns per sistemi con memoria limitata

  • Ridurre la lunghezza ed eliminare quante più costanti di stringa ansible per ridurre lo spazio del codice

  • Considerare attentamente i compromessi degli algoritmi rispetto alle tabelle di consultazione dove necessario

  • Essere consapevoli di come vengono allocati diversi tipi di variabili.

    • Le costanti sono probabilmente nello spazio del codice.
    • Le variabili statiche sono probabilmente in posizioni di memoria fisse. Evita questi se ansible.
    • I parametri sono probabilmente memorizzati nello stack o nei registri.
    • Le variabili locali possono anche essere allocate dallo stack. Non dichiarare array o stringhe locali di grandi dimensioni se il codice potrebbe esaurire lo spazio di stack nelle condizioni peggiori.
    • Potresti non avere un heap: potrebbe non esserci un sistema operativo per gestire l’heap per te. È accettabile? Hai bisogno di una funzione malloc ()?

Un trucco utile nelle applicazioni è creare un fondo di memoria per la giornata piovosa. Allocare blocco singolo all’avvio che è abbastanza grande da essere sufficiente per le attività di pulizia. Se malloc / new falliscono, rilascia il fondo rainy day e posta un messaggio per far sapere all’utente che le risorse sono strette e che dovrebbero salvare e molto presto. Questa era una tecnica utilizzata in molte applicazioni Mac circa nel 1990.

Un ottimo modo per limitare i requisiti di memoria è affidarsi il più ansible su libc o altre librerie standard che possono essere collegate dynamicmente. Ogni altra DLL o object condiviso che devi includere nel tuo progetto è una parte significativa della memoria che potresti essere in grado di evitare di masterizzare.

Inoltre, fai uso di unioni e campi bit, dove applicabile, carica solo la parte dei dati su cui il tuo programma sta lavorando in memoria, e assicurati di compilare l’opzione -Os (in gcc o equivalente del tuo compilatore) per ottimizzare per la dimensione del programma.

Ho una presentazione della Embedded Systems Conference disponibile su questo argomento. È del 2001, ma è ancora molto applicabile. Vedi carta

Inoltre, se puoi scegliere l’architettura del dispositivo di destinazione, con qualcosa come un moderno ARM con Thumb V2, o un PowerPC con VLE, ​​o MIPS con MIPS16, o selezionando obiettivi compatti noti come Infineon TriCore o la famiglia SH è un ottima opzione Per non parlare della famiglia NEC V850E che è ben compatta. O passare a un AVR che ha un’eccellente compattezza del codice (ma è una macchina a 8 bit). Tutto tranne un RISC a 32 bit a lunghezza fissa è una buona scelta!

Oltre ai suggerimenti forniti da altri, ricorda che le variabili locali dichiarate nelle tue funzioni saranno normalmente allocate nello stack.

Se la memoria dello stack è limitata o si desidera ridurre la dimensione dello stack per fare spazio a più heap o RAM globale, considerare quanto segue:

  1. Appiattisci l’albero delle chiamate per ridurre il numero di variabili nello stack in qualsiasi momento.
  2. Converti grandi variabili locali in globali (diminuisce la quantità di pila utilizzata, ma aumenta la quantità di RAM globale utilizzata). Le variabili possono essere dichiarate:

    • Ambito globale: visibile a tutto il programma
    • Statico nell’ambito del file: visibile nello stesso file
    • Statico nell’ambito della funzione: visibile all’interno della funzione
    • NOTA: Indipendentemente da ciò, se queste modifiche vengono apportate, è necessario diffidare dei problemi con il codice di reentrant se si dispone di un ambiente di preemptive .

Molti sistemi incorporati non dispongono di una diagnostica del monitor stack per assicurare che venga rilevato un overflow dello stack , pertanto è necessaria un’analisi.

PS: Bonus per l’uso appropriato di Stack Overflow ?