Qual è l'approccio migliore quando si scrivono le funzioni per il software incorporato al fine di ottenere prestazioni migliori? [chiuso]


13

Ho visto alcune librerie di microcontrollori e le loro funzioni fanno una cosa alla volta. Ad esempio, qualcosa del genere:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Poi arrivano altre funzioni che usano questo codice a 1 riga contenente una funzione per altri scopi. Per esempio:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Non sono sicuro, ma credo in questo modo sarebbe creare più chiamate ai salti e creare sovraccarico di impilare gli indirizzi di ritorno ogni volta che viene chiamata o chiusa una funzione. E ciò renderebbe lento il programma, giusto?

Ho cercato e ovunque dicono che la regola del pollice della programmazione è che una funzione dovrebbe svolgere solo un compito.

Quindi, se scrivo direttamente un modulo funzione InitModule che imposta l'orologio, aggiunge la configurazione desiderata e fa qualcos'altro senza chiamare le funzioni. È un cattivo approccio quando si scrive un software incorporato?


MODIFICA 2:

  1. Sembra che molte persone abbiano capito questa domanda come se stessi cercando di ottimizzare un programma. No, non ho intenzione di farlo . Lascio che sia il compilatore a farlo, perché sarà sempre (spero di no!) Meglio di me.

  2. Tutte le responsabilità su di me per aver scelto un esempio che rappresenta un codice di inizializzazione . La domanda non ha alcuna intenzione di considerare le chiamate di funzione effettuate allo scopo di inizializzazione. La mia domanda è : spezzare un determinato compito in piccole funzioni di più righe ( quindi in linea è fuori discussione ) in esecuzione all'interno di un ciclo infinito ha qualche vantaggio rispetto alla scrittura di una funzione lunga senza alcuna funzione nidificata?

Si prega di considerare la leggibilità definita nella risposta di @Jonk .


28
Sei molto ingenuo (non inteso come un insulto) se ritieni che qualsiasi compilatore ragionevole trasformi ciecamente il codice come scritto in binari come scritto. La maggior parte dei compilatori moderni è abbastanza brava a identificare quando una routine è meglio definita e anche quando una posizione di registro vs RAM dovrebbe essere usata per contenere una variabile. Segui le due regole di ottimizzazione: 1) non ottimizzare. 2) non ottimizzare ancora . Rendi leggibile e gestibile il tuo codice e POI solo dopo aver profilato un sistema funzionante, cerca di ottimizzare.
akohlsmith il

10
@akohlsmith IIRC le tre regole di ottimizzazione sono: 1) No ! 2) No davvero! 3) Profilo prima, poi e solo allora ottimizza se devi - Michael_A._Jackson
esoterik

3
Ricorda solo che "l'ottimizzazione prematura è la radice di tutti i mali (o almeno la maggior parte di essi) nella programmazione" - Knuth
Mawg afferma di ripristinare Monica il

1
@Mawg: la parola chiave è prematura . (Come spiega il prossimo paragrafo in quel documento. Letteralmente la frase successiva: "Eppure non dovremmo rinunciare alle nostre opportunità in quel 3% critico.") Non ottimizzare fino al necessario - non avrai trovato il lento bit fino a quando non hai qualcosa da profilare, ma non impegnarti nella pessimizzazione, ad esempio utilizzando strumenti palesemente sbagliati per il lavoro.
cHao,

1
@Mawg Non so perché ho ricevuto risposte / feedback relativi all'ottimizzazione, dal momento che non ho mai menzionato la parola e intendo farlo. La domanda è molto di più su come scrivere le funzioni nella programmazione integrata per ottenere prestazioni migliori.
MaNyYaCk,

Risposte:


28

Probabilmente, nel tuo esempio le prestazioni non contano, poiché il codice viene eseguito una sola volta all'avvio.

Una regola pratica che uso: scrivi il tuo codice il più leggibile possibile e inizia a ottimizzare solo se noti che il tuo compilatore non sta facendo la sua magia.

Il costo di una chiamata di funzione in un ISR potrebbe essere uguale a quello di una chiamata di funzione durante l'avvio in termini di archiviazione e tempistica. Tuttavia, i requisiti di temporizzazione durante tale ISR potrebbero essere molto più critici.

Inoltre, come già notato da altri, il costo (e il significato del "costo") di una chiamata di funzione differisce per piattaforma, compilatore, impostazione di ottimizzazione del compilatore e requisiti dell'applicazione. Ci sarà un'enorme differenza tra un 8051 e una corteccia-m7, un pacemaker e un interruttore della luce.


6
Il secondo paragrafo dell'IMO dovrebbe essere in grassetto e in alto. Non c'è niente di sbagliato nella scelta degli algoritmi e delle strutture dati giusti, ma preoccuparsi della funzione di overhead delle chiamate di funzione a meno che non si sia scoperto che si tratta di un vero collo di bottiglia è sicuramente un'ottimizzazione prematura, e dovrebbe essere evitato.
Fondi Monica's Lawsuit,

11

Non mi viene in mente alcun vantaggio (ma vedi la nota a JasonS in fondo), racchiudendo una riga di codice come funzione o subroutine. Tranne forse che puoi nominare la funzione qualcosa di "leggibile". Ma puoi anche commentare la linea. E dato che racchiudere una riga di codice in una funzione costa memoria di codice, spazio di stack e tempo di esecuzione, mi sembra che sia principalmente controproducente. In una situazione di insegnamento? Potrebbe avere un senso. Ma ciò dipende dalla classe degli studenti, dalla loro preparazione in anticipo, dal curriculum e dall'insegnante. Principalmente, penso che non sia una buona idea. Ma questa è la mia opinione.

Il che ci porta alla conclusione. La vostra vasta area di domande è stata, per decenni, oggetto di un dibattito e rimane ancora oggi un argomento di dibattito. Quindi, almeno mentre leggo la tua domanda, mi sembra una domanda basata sull'opinione (come l'hai fatta tu.)

Potrebbe essere allontanato dall'essere basato sull'opinione come è, se si dovesse essere più dettagliati sulla situazione e descrivere attentamente gli obiettivi che si ritenevano primari. Migliore è la definizione degli strumenti di misurazione, più obiettivi potrebbero essere le risposte.


In linea di massima, si desidera effettuare le seguenti operazioni per qualsiasi codice. (Per il seguito, suppongo che stiamo confrontando diversi approcci che raggiungono tutti gli obiettivi. Ovviamente, qualsiasi codice che non riesce a svolgere le attività necessarie è peggiore del codice che ha successo, indipendentemente da come è scritto.)

  1. Sii coerente con il tuo approccio, in modo che un'altra lettura del tuo codice possa sviluppare una comprensione di come approcci il tuo processo di codifica. Essere incoerenti è probabilmente il crimine peggiore possibile. Non solo rende difficile per gli altri, ma rende difficile per te tornare al codice anni dopo.
  2. Nella misura del possibile, prova a sistemare le cose in modo che l'inizializzazione di varie sezioni funzionali possa essere eseguita indipendentemente dall'ordinamento. Dove è richiesto l'ordinazione, se è dovuto a un accoppiamento stretto di due funzioni secondarie altamente correlate, prendere in considerazione una singola inizializzazione per entrambi in modo che possa essere riordinato senza causare danni. Se ciò non è possibile, documentare il requisito di ordinazione di inizializzazione.
  3. Incapsulare la conoscenza esattamente in un posto, se possibile. Le costanti non devono essere duplicate ovunque nel codice. Le equazioni che risolvono per alcune variabili dovrebbero esistere in un unico posto. E così via. Se ti ritrovi a copiare e incollare un insieme di linee che eseguono alcuni comportamenti necessari in una varietà di posizioni, considera un modo per acquisire tale conoscenza in un unico luogo e utilizzarla dove necessario. Ad esempio, se si dispone di una struttura ad albero che deve essere percorsa in un modo specifico, non farloreplicare il codice tree-walking in ogni punto in cui è necessario scorrere i nodi dell'albero. Invece, cattura il metodo del camminare sugli alberi in un posto e usalo. In questo modo, se l'albero cambia e il metodo di camminata cambia, hai solo un posto di cui preoccuparti e tutto il resto del codice "funziona bene".
  4. Se distribuisci tutte le tue routine su un enorme foglio di carta piatto, con le frecce che le collegano come vengono chiamate da altre routine, vedrai in ogni applicazione che ci saranno "gruppi" di routine che hanno un sacco di frecce tra loro ma solo poche frecce fuori dal gruppo. Quindi ci saranno confini naturali di routine strettamente accoppiate e connessioni vagamente accoppiate tra altri gruppi di routine strettamente accoppiate. Usa questo fatto per organizzare il tuo codice in moduli. Ciò limiterà sostanzialmente la complessità apparente del codice.

Quanto sopra è generalmente vero su tutta la codifica. Non ho discusso dell'uso di parametri, variabili globali locali o statiche, ecc. Il motivo è che per la programmazione integrata lo spazio dell'applicazione pone spesso nuovi vincoli estremi e molto significativi ed è impossibile discuterne tutti senza discutere di ogni applicazione incorporata. E questo non sta accadendo qui, comunque.

Questi vincoli possono essere uno (e più) di questi:

  • Gravi limiti di costo che richiedono MCU estremamente primitive con RAM minuscola e quasi nessun conteggio dei pin I / O. Per questi, si applicano interi nuovi set di regole. Ad esempio, potrebbe essere necessario scrivere nel codice assembly perché non c'è molto spazio nel codice. Potrebbe essere necessario utilizzare SOLO variabili statiche perché l'uso di variabili locali è troppo costoso e richiede tempo. Potrebbe essere necessario evitare l'uso eccessivo di subroutine perché (ad esempio, alcune parti Microchip PIC) ci sono solo 4 registri hardware in cui memorizzare gli indirizzi di ritorno della subroutine. Quindi potresti dover "appiattire" in modo drammatico il tuo codice. Eccetera.
  • Gravi limiti di potenza che richiedono un codice attentamente predisposto per l'avvio e l'arresto della maggior parte dell'MCU e la limitazione severa del tempo di esecuzione del codice quando si esegue a piena velocità. Ancora una volta, questo potrebbe richiedere alcuni codici di assemblaggio, a volte.
  • Requisiti di temporizzazione severi. Ad esempio, ci sono volte in cui ho dovuto assicurarmi che la trasmissione di uno drain aperto 0 dovesse richiedere ESATTAMENTE lo stesso numero di cicli della trasmissione di un 1. E che anche il campionamento di questa stessa linea doveva essere eseguito con una fase relativa esatta a questo tempismo. Ciò significava che C NON poteva essere usato qui. L'UNICO modo possibile per garantire tale garanzia è creare con cura il codice di assemblaggio. (E anche allora, non sempre su tutti i design ALU.)

E così via. (Anche il codice di cablaggio per la strumentazione medica critica per la vita ha un suo intero mondo.)

Il risultato qui è che la codifica incorporata spesso non è gratuita, in cui è possibile programmare come su una workstation. Esistono spesso motivi severi e competitivi per un'ampia varietà di vincoli molto difficili. E questi possono fortemente discutere contro le risposte più tradizionali e di borsa .


Per quanto riguarda la leggibilità, trovo che il codice sia leggibile se è scritto in modo coerente che posso imparare mentre lo leggo. E dove non c'è un tentativo deliberato di offuscare il codice. Non c'è davvero molto di più richiesto.

Il codice leggibile può essere abbastanza efficiente e può soddisfare tutti i requisiti sopra menzionati. La cosa principale è che capisci esattamente cosa produce ogni riga di codice che scrivi a livello di assembly o macchina, mentre lo scrivi. Il C ++ pone un grave onere per il programmatore qui perché ci sono molte situazioni in cui frammenti identici di codice C ++ in realtà generano frammenti diversi di codice macchina con prestazioni notevolmente diverse. Ma C, generalmente, è principalmente una lingua "ciò che vedi è ciò che ottieni". Quindi è più sicuro al riguardo.


EDIT per JasonS:

Uso C dal 1978 e C ++ dal 1987 circa e ho avuto molta esperienza nell'uso di entrambi i mainframe, i minicomputer e (principalmente) le applicazioni integrate.

Jason fa apparire un commento sull'uso di 'inline' come modificatore. (Nella mia prospettiva, questa è una funzionalità relativamente "nuova" perché semplicemente non esisteva per forse metà della mia vita o più usando C e C ++.) L'uso di funzioni in linea può effettivamente effettuare tali chiamate (anche per una linea di codice) abbastanza pratico. Ed è molto meglio, ove possibile, che usare una macro a causa della digitazione che il compilatore può applicare.

Ma ci sono anche dei limiti. Il primo è che non puoi fare affidamento sul compilatore per "prendere il suggerimento". Potrebbe o no. E ci sono buoni motivi per non dare il suggerimento. (Per un esempio ovvio, se viene preso l'indirizzo della funzione, ciò richiede l'istanza della funzione e l'uso dell'indirizzo per effettuare la chiamata ... richiederà una chiamata. Il codice non può essere incorporato allora.) Ci sono anche altri motivi. I compilatori possono avere un'ampia varietà di criteri in base ai quali giudicano come gestire il suggerimento. E come programmatore, questo significa che devidedicare un po 'di tempo a conoscere quell'aspetto del compilatore oppure è probabile che tu prenda decisioni basate su idee imperfette. Quindi aggiunge un onere sia allo scrittore del codice che a qualsiasi lettore e anche a chiunque abbia intenzione di trasferire il codice su qualche altro compilatore.

Inoltre, i compilatori C e C ++ supportano la compilazione separata. Ciò significa che possono compilare un pezzo di codice C o C ++ senza compilare nessun altro codice correlato per il progetto. Per incorporare il codice, supponendo che il compilatore possa altrimenti scegliere di farlo, non solo deve avere la dichiarazione "nell'ambito" ma deve anche avere la definizione. Di solito, i programmatori lavoreranno per assicurarsi che ciò avvenga se utilizzano "inline". Ma è facile che si insinuino errori.

In generale, anche se uso in linea dove ritengo sia appropriato, tendo ad assumere che non posso fare affidamento su di esso. Se le prestazioni sono un requisito significativo e penso che l'OP abbia già scritto chiaramente che si è verificato un notevole calo delle prestazioni quando sono andati su un percorso più "funzionale", allora certamente sceglierei di evitare di fare affidamento su inline come pratica di codifica e seguirebbe invece un modello leggermente diverso, ma del tutto coerente di scrittura del codice.

Un'ultima nota su "inline" e le definizioni "in ambito" per una fase di compilazione separata. È possibile (non sempre affidabile) che il lavoro venga eseguito nella fase di collegamento. Ciò può accadere se e solo se un compilatore C / C ++ nasconde abbastanza dettagli nei file oggetto per consentire a un linker di agire su richieste "inline". Personalmente non ho sperimentato un sistema di linker (al di fuori di Microsoft) che supporti questa funzionalità. Ma può succedere. Ancora una volta, se si debba fare affidamento o meno dipenderà dalle circostanze. Ma di solito suppongo che questo non sia stato spinto sul linker, a meno che non lo sappia altrimenti sulla base di buone prove. E se faccio affidamento su di esso, sarà documentato in un posto di rilievo.


C ++

Per coloro che sono interessati, ecco un esempio del motivo per cui rimango abbastanza cauto nei confronti del C ++ quando codifico le applicazioni incorporate, nonostante la sua pronta disponibilità oggi. Lancerò alcuni termini che penso che tutti i programmatori C ++ incorporati debbano conoscere a freddo :

  • specializzazione parziale del modello
  • VTables
  • oggetto base virtuale
  • frame di attivazione
  • cornice di attivazione svolgersi
  • uso di puntatori intelligenti nei costruttori e perché
  • ottimizzazione del valore di ritorno

Questo è solo un breve elenco. Se non sai già tutto di quei termini e del perché li ho elencati (e molti altri non li ho elencati qui) allora sconsiglierei l'uso del C ++ per il lavoro incorporato, a meno che non sia un'opzione per il progetto .

Diamo una rapida occhiata alla semantica delle eccezioni C ++ per avere solo un sapore.

UNB

UN

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

UN

B

Il compilatore C ++ vede la prima chiamata a foo () e può semplicemente consentire il verificarsi di un normale frame di attivazione, se foo () genera un'eccezione. In altre parole, il compilatore C ++ sa che a questo punto non è necessario alcun codice aggiuntivo per supportare il processo di svolgimento del frame coinvolto nella gestione delle eccezioni.

Ma una volta che String s è stato creato, il compilatore C ++ sa che deve essere correttamente distrutto prima che possa essere consentito lo svolgimento di un frame, se in seguito si verifica un'eccezione. Quindi la seconda chiamata a foo () è semanticamente diversa dalla prima. Se la seconda chiamata a foo () genera un'eccezione (cosa che può o non può fare), il compilatore deve aver inserito un codice progettato per gestire la distruzione di String s prima di lasciare che si verifichi il consueto svolgimento del frame. Questo è diverso dal codice richiesto per la prima chiamata a foo ().

(È possibile aggiungere ulteriori decorazioni in C ++ per aiutare a limitare questo problema. Ma il fatto è che i programmatori che usano C ++ devono semplicemente essere molto più consapevoli delle implicazioni di ogni riga di codice che scrivono.)

A differenza del malloc di C, il nuovo C ++ utilizza le eccezioni per segnalare quando non è in grado di eseguire l'allocazione della memoria non elaborata. Così sarà 'dynamic_cast'. (Vedi il 3 ° ed. Di Stroustrup, Il linguaggio di programmazione C ++, pagine 384 e 385 per le eccezioni standard in C ++.) I compilatori possono consentire di disabilitare questo comportamento. Ma in generale dovrete sostenere un certo overhead a causa di prologhi ed epiloghi di gestione delle eccezioni correttamente formati nel codice generato, anche quando le eccezioni in realtà non hanno luogo e anche quando la funzione in fase di compilazione non ha effettivamente blocchi di gestione delle eccezioni. (Stroustrup l'ha lamentato pubblicamente.)

Senza una specializzazione parziale dei template (non tutti i compilatori C ++ lo supportano), l'uso dei template può portare al disastro per la programmazione integrata. Senza di essa, la fioritura del codice rappresenta un grave rischio che potrebbe uccidere in un lampo un progetto incorporato con memoria ridotta.

Quando una funzione C ++ restituisce un oggetto, viene creato e distrutto un temporaneo compilatore senza nome. Alcuni compilatori C ++ possono fornire un codice efficiente se viene utilizzato un costruttore di oggetti nell'istruzione return, anziché un oggetto locale, riducendo le esigenze di costruzione e distruzione di un oggetto. Ma non tutti i compilatori lo fanno e molti programmatori C ++ non sono nemmeno a conoscenza di questa "ottimizzazione del valore di ritorno".

Fornire a un costruttore di oggetti un singolo tipo di parametro può consentire al compilatore C ++ di trovare un programmatore in un percorso di conversione tra due tipi in modi completamente inaspettati. Questo tipo di comportamento "intelligente" non fa parte di C.

Una clausola catch che specifica un tipo base "suddivide" un oggetto derivato generato, poiché l'oggetto generato viene copiato usando il "tipo statico" della clausola catch e non il "tipo dinamico" dell'oggetto. Una fonte non insolita di sofferenza delle eccezioni (quando ritieni di poterti permettere persino eccezioni nel tuo codice incorporato).

I compilatori C ++ possono generare automaticamente costruttori, distruttori, copiatori e operatori di assegnazione per te, con risultati indesiderati. Ci vuole tempo per guadagnare facilità con i dettagli di questo.

Il passaggio di matrici di oggetti derivati ​​a una funzione che accetta matrici di oggetti di base, raramente genera avvisi del compilatore ma produce quasi sempre comportamenti errati.

Poiché il C ++ non invoca il distruttore di oggetti parzialmente costruiti quando si verifica un'eccezione nel costruttore di oggetti, la gestione delle eccezioni nei costruttori di solito impone "puntatori intelligenti" per garantire che i frammenti costruiti nel costruttore vengano correttamente distrutti se si verifica un'eccezione lì . (Vedi Stroustrup, pagina 367 e 368.) Questo è un problema comune nello scrivere buone classi in C ++, ma ovviamente evitato in C poiché C non ha la semantica di costruzione e distruzione incorporata. Scrivere codice adeguato per gestire la costruzione di oggetti secondari all'interno di un oggetto significa scrivere codice che deve affrontare questo problema semantico univoco in C ++; in altre parole "scrivere intorno" comportamenti semantici in C ++.

C ++ può copiare oggetti passati a parametri oggetto. Ad esempio, nei seguenti frammenti, la chiamata "rA (x);" può far sì che il compilatore C ++ invochi un costruttore per il parametro p, per poi chiamare il costruttore copia per trasferire l'oggetto x al parametro p, quindi un altro costruttore per l'oggetto di ritorno (un temporaneo senza nome) della funzione rA, che ovviamente è copiato dal parametro p. Peggio ancora, se la classe A ha i suoi oggetti che hanno bisogno di essere costruiti, questo può teletrasportarsi in modo disastroso. (Il programmatore AC eviterebbe la maggior parte di questa spazzatura, ottimizzando a mano poiché i programmatori C non hanno una sintassi così utile e devono esprimere tutti i dettagli uno alla volta.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Infine, una breve nota per i programmatori C. longjmp () non ha un comportamento portatile in C ++. (Alcuni programmatori C usano questo come una sorta di meccanismo di "eccezione".) Alcuni compilatori C ++ tenteranno effettivamente di sistemare le cose per ripulire quando viene preso il longjmp, ma quel comportamento non è portabile in C ++. Se il compilatore ripulisce gli oggetti costruiti, non è portatile. Se il compilatore non li ripulisce, gli oggetti non vengono distrutti se il codice lascia l'ambito degli oggetti costruiti a causa del longjmp e il comportamento non è valido. (Se l'uso di longjmp in foo () non lascia un ambito, allora il comportamento potrebbe andare bene.) Questo non è troppo spesso usato dai programmatori C incorporati ma dovrebbero essere consapevoli di questi problemi prima di usarli.


4
Questo tipo di funzioni utilizzate solo una volta non vengono mai compilate come chiamate di funzione, il codice viene semplicemente inserito lì senza alcuna chiamata.
Dorian,

6
@Dorian - il tuo commento potrebbe essere vero in determinate circostanze per alcuni compilatori. Se la funzione è statica all'interno del file, il compilatore ha l' opzione per rendere il codice in linea. se è visibile esternamente, quindi, anche se non viene mai effettivamente chiamato, ci deve essere un modo per rendere richiamabile la funzione.
Il

1
@jonk - Un altro trucco che non hai menzionato in una buona risposta è quello di scrivere semplici funzioni macro che eseguono l'inizializzazione o la configurazione come codice inline espanso. Ciò è particolarmente utile sui processori molto piccoli in cui la profondità della chiamata RAM / stack / funzione è limitata.
Il

@ ʎəʞouɐɪ Sì, mi sono perso a discutere di macro in C. Quelli sono deprecati in C ++, ma una discussione su questo punto potrebbe essere utile. Potrei affrontarlo, se riesco a capire qualcosa di utile da scrivere al riguardo.
Jon

1
@jonk - Non sono completamente d'accordo con la tua prima frase. Un esempio come quello inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }che viene chiamato in numerosi luoghi è un candidato perfetto.
Jason S,

8

1) Codice per leggibilità e manutenibilità prima. L'aspetto più importante di qualsiasi base di codice è che è ben strutturato. Un software ben scritto tende ad avere meno errori. Potrebbe essere necessario apportare modifiche in un paio di settimane / mesi / anni, e aiuta immensamente se il tuo codice è bello da leggere. O forse qualcun altro deve fare un cambiamento.

2) Le prestazioni del codice che viene eseguito una volta non contano molto. Cura dello stile, non delle prestazioni

3) Anche il codice in loop stretti deve essere innanzitutto corretto. Se si verificano problemi di prestazioni, ottimizzare una volta che il codice è corretto.

4) Se devi ottimizzare, devi misurare! Non importa se pensi o qualcuno ti dice che static inlineè solo una raccomandazione per il compilatore. Devi dare un'occhiata a cosa fa il compilatore. Devi anche misurare se l'allineamento ha migliorato le prestazioni. Nei sistemi embedded, devi anche misurare la dimensione del codice, poiché la memoria del codice è generalmente piuttosto limitata. Questa è la regola più importante che distingue l'ingegneria dalle congetture. Se non l'hai misurato, non ha aiutato. L'ingegneria sta misurando. La scienza lo sta scrivendo;)


2
L'unica critica che ho del tuo post altrimenti eccellente è il punto 2). È vero che le prestazioni del codice di inizializzazione sono irrilevanti, ma in un ambiente incorporato le dimensioni possono avere importanza. (Ma questo non prevale sul punto 1; inizia a ottimizzare le dimensioni quando è necessario - e non prima)
Martin Bonner supporta Monica il

2
Le prestazioni del codice di inizializzazione potrebbero inizialmente essere irrilevanti. Quando si aggiunge la modalità a basso consumo e si desidera ripristinare rapidamente per gestire l'evento di riattivazione, diventa rilevante.
berendi - protestando contro il

5

Quando una funzione viene chiamata solo in un posto (anche all'interno di un'altra funzione), il compilatore inserisce sempre il codice in quel posto invece di chiamare realmente la funzione. Se la funzione viene chiamata in più punti di quanto non abbia senso utilizzare una funzione almeno dal punto di vista della dimensione del codice.

Dopo la compilazione il codice non avrà più chiamate, ma la leggibilità sarà notevolmente migliorata.

Inoltre, vorrai avere ad esempio il codice di inizializzazione ADC nella stessa libreria con altre funzioni ADC non presenti nel file c principale.

Molti compilatori ti consentono di specificare diversi livelli di ottimizzazione per la velocità o la dimensione del codice, quindi se hai una piccola funzione chiamata in molti punti, la funzione sarà "inline", copiata lì invece di chiamare.

L'ottimizzazione per la velocità incorporerà le funzioni in più posti possibili, l'ottimizzazione per la dimensione del codice chiamerà la funzione, tuttavia, quando una funzione viene chiamata in un solo posto, nel tuo caso sarà sempre "inline".

Codice come questo:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

compilerà per:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

senza usare alcuna chiamata.

E la risposta alla tua domanda, nel tuo esempio o simile, la leggibilità del codice non influisce sulle prestazioni, nulla è molto in termini di velocità o dimensioni del codice. È comune utilizzare più chiamate solo per rendere leggibile il codice, alla fine vengono rispettati come codice in linea.

Aggiorna per specificare che le precedenti istruzioni non sono valide per compilatori di versione gratuita appositamente danneggiati come la versione gratuita di Microchip XCxx. Questo tipo di chiamate di funzione è una miniera d'oro per Microchip per mostrare quanto sia migliore la versione a pagamento e se la compili troverai nell'ASM esattamente tutte le chiamate che hai nel codice C.

Inoltre non è per i programmatori stupidi che si aspettano di utilizzare il puntatore a una funzione incorporata.

Questa è la sezione elettronica, non C ++ generale o la sezione di programmazione, la domanda riguarda la programmazione di microcontrollori in cui qualsiasi compilatore decente eseguirà l'ottimizzazione di cui sopra per impostazione predefinita.

Quindi, per favore, smetti di effettuare il downgrade solo perché in rari casi insoliti questo potrebbe non essere vero.


15
Se il codice diventa inline o meno è un problema specifico dell'implementazione del fornitore del compilatore; anche l'uso della parola chiave inline non garantisce il codice inline. È un suggerimento per il compilatore. Certamente i bravi compilatori incorporeranno le funzioni usate solo una volta se ne sono a conoscenza. Di solito non lo farà se ci sono oggetti "volatili" nell'ambito.
Peter Smith,

9
Questa risposta non è vera. Come dice @PeterSmith, e secondo la specifica del linguaggio C, il compilatore ha l' opzione di incorporare il codice ma potrebbe non farlo, e in molti casi non lo farà. Esistono così tanti compilatori diversi nel mondo per così tanti processori target diversi che rendono il tipo di istruzione coperta in questa risposta e supponendo che tutti i compilatori inseriranno il codice in linea quando hanno solo l'opzione non è sostenibile.
uɐɪ

2
@ ʎəʞouɐɪ Stai indicando rari casi in cui non è possibile e sarebbe una cattiva idea non chiamare una funzione in primo luogo. Non ho mai visto un compilatore così stupido da usare davvero call nel semplice esempio dato dall'OP.
Dorian,

6
Nei casi in cui queste funzioni vengono chiamate una sola volta, l'ottimizzazione della funzione di richiamo è praticamente un problema. Il sistema ha davvero bisogno di recuperare ogni singolo ciclo di clock durante l'installazione? Come nel caso dell'ottimizzazione ovunque, scrivi codice leggibile e ottimizza solo se la profilazione mostra che è necessario .
Baldrickk,

5
@MSalters Non mi preoccupo di ciò che il compilatore finisce qui - più su come il programmatore si avvicina. Non vi è alcun impatto negativo o trascurabile a causa della rottura dell'inizializzazione, come si vede nella domanda.
Baldrickk

2

Prima di tutto, non esiste il meglio o il peggio; è tutta una questione di opinione. Hai ragione nel dire che questo è inefficiente. Può essere ottimizzato o meno; dipende. Di solito vedrai questi tipi di funzioni, orologio, GPIO, timer, ecc. In file / directory separati. I compilatori in genere non sono stati in grado di ottimizzare attraverso queste lacune. Ce n'è uno che conosco ma che non è ampiamente usato per cose come questa.

File singolo:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Scegliere un target e un compilatore a scopo dimostrativo.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

Questo è ciò che la maggior parte delle risposte qui ti dicono, che sei ingenuo e che tutto viene ottimizzato e le funzioni vengono rimosse. Bene, non vengono rimossi perché definiti globalmente per impostazione predefinita. Possiamo rimuoverli se non necessari al di fuori di questo unico file.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

li rimuove ora quando sono allineati.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Ma la realtà è quando prendi il fornitore di chip o le librerie BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Sicuramente inizierai ad aggiungere un sovraccarico, che ha un notevole costo in termini di prestazioni e spazio. Da pochi a cinque per cento di ciascuno a seconda di quanto piccola sia ciascuna funzione.

Perché lo si fa comunque? Alcuni di questi sono l'insieme di regole che i professori insegnerebbero o continueranno a rendere più semplice la classificazione dei codici. Le funzioni devono adattarsi a una pagina (indietro quando hai stampato il tuo lavoro su carta), non farlo, non farlo, ecc. Gran parte di ciò è creare librerie con nomi comuni per obiettivi diversi. Se hai decine di famiglie di microcontrollori, alcuni dei quali condividono periferiche e altri no, forse tre o quattro diversi tipi di UART misti tra le famiglie, diversi GPIO, controller SPI, ecc. Puoi avere una funzione gpio_init () generica, get_timer_count (), ecc. E riutilizzare quelle astrazioni per le diverse periferiche.

Diventa un caso principalmente di manutenibilità e progettazione del software, con qualche possibile leggibilità. Mantenibilità, leggibilità e prestazioni che non puoi avere tutto; puoi sceglierne solo uno o due alla volta, non tutti e tre.

Questa è una domanda molto basata sull'opinione, e quanto sopra mostra i tre principali modi in cui può andare. Per quanto riguarda quale percorso è MIGLIORE che è rigorosamente opinione. Fare tutto il lavoro in un'unica funzione? Una domanda basata sull'opinione, alcune persone propendono per le prestazioni, alcune definiscono la modularità e la loro versione di leggibilità come MIGLIORE. L'interessante questione di ciò che molti chiamano leggibilità è estremamente dolorosa; per "vedere" il codice devi avere 50-10.000 file aperti contemporaneamente e in qualche modo provare a vedere linearmente le funzioni in esecuzione per vedere cosa sta succedendo. Trovo che l'opposto della leggibilità, ma altri lo trovano leggibile poiché ogni elemento si adatta alla finestra dello schermo / dell'editor e può essere consumato completamente dopo aver memorizzato le funzioni chiamate e / o avere un editor che può entrare e uscire ogni funzione all'interno di un progetto.

Questo è un altro grande fattore quando vedi varie soluzioni. Editor di testo, IDE, ecc. Sono molto personali e vanno oltre vi vs Emacs. Efficienza di programmazione, le linee al giorno / al mese aumentano se si è a proprio agio ed efficienti con lo strumento che si sta utilizzando. Le caratteristiche dello strumento possono / saranno intenzionalmente o meno orientate verso il modo in cui i fan di quello strumento scrivono il codice. E di conseguenza se un individuo sta scrivendo queste librerie il progetto riflette in una certa misura queste abitudini. Anche se si tratta di una squadra, le principali abitudini / preferenze dello sviluppatore principale o del capo possono essere forzate sul resto della squadra.

Standard di codifica in cui sono sepolte molte preferenze personali, di nuovo vi vs Emacs molto religiosi, tabulazioni contro spazi, come sono allineate le parentesi, ecc.

Come dovresti scrivere il tuo? Comunque tu voglia, non c'è davvero una risposta sbagliata se funziona. C'è un codice cattivo o rischioso certo, ma se scritto in modo tale da poterlo mantenere secondo necessità, soddisfa i tuoi obiettivi di progettazione, rinuncia alla leggibilità e un po 'di manutenibilità se le prestazioni sono importanti o viceversa. Ti piacciono i nomi di variabili brevi in ​​modo che una singola riga di codice si adatti alla larghezza della finestra dell'editor? O nomi troppo descrittivi per evitare confusione, ma la leggibilità diminuisce perché non è possibile ottenere una riga su una pagina; ora è visivamente interrotto, rovinando il flusso.

Non hai intenzione di colpire una corsa a casa la prima volta a bat. Potrebbero / dovrebbero essere necessari decenni per definire veramente il tuo stile. Allo stesso tempo, in quel momento, il tuo stile potrebbe cambiare, inclinandoti in un modo per un po ', quindi inclinandone un altro.

Sentirai molto non ottimizzare, mai ottimizzare e ottimizzazione prematura. Ma come mostrato, progetti come questo fin dall'inizio creano problemi di prestazioni, quindi inizi a vedere gli hack per risolvere quel problema piuttosto che riprogettare dall'inizio per eseguire. Sono d'accordo che ci sono situazioni, una singola funzione alcune righe di codice che puoi provare a manipolare duramente il compilatore in base al timore di ciò che il compilatore farà altrimenti (nota con esperienza che questo tipo di codifica diventa facile e naturale, ottimizzando mentre scrivi sapendo come il compilatore sta per compilare il codice), quindi vuoi confermare dove si trova realmente il ladro di cicli, prima di attaccarlo.

È inoltre necessario progettare il codice per l'utente in una certa misura. Se questo è il tuo progetto, sei l'unico sviluppatore; è quello che vuoi. Se stai cercando di creare una libreria da regalare o vendere, probabilmente vorrai far sembrare il tuo codice come tutte le altre librerie, centinaia o migliaia di file con funzioni minuscole, nomi di funzioni lunghe e nomi di variabili lunghe. Nonostante i problemi di leggibilità e prestazioni, IMO troverete che più persone saranno in grado di utilizzare quel codice.


4
Veramente? Quali "alcuni target" e "alcuni compilatori" usi posso chiedere?
Dorian,

Mi sembra più un ARM8 a 32/64 bit, forse da un lampo PI poi un solito microcontrollore. Hai letto la prima frase nella domanda?
Dorian,

Bene, il compilatore non rimuove le funzioni globali inutilizzate, ma il linker lo fa. Se è configurato e utilizzato correttamente, non verranno visualizzati nell'eseguibile.
berendi - protestando contro il

Se qualcuno si chiede quale compilatore sia in grado di ottimizzare tra le lacune dei file: i compilatori IAR supportano la compilazione di più file (è così che la chiamano), che consente l'ottimizzazione incrociata dei file. Se si lanciano tutti i file c / cpp su di esso in una volta, si finisce con un eseguibile che contiene una singola funzione: main. I vantaggi in termini di prestazioni possono essere piuttosto profondi.
Arsenal,

3
@Arsenal Naturalmente gcc supporta l'integrazione, anche attraverso le unità di compilazione, se chiamato correttamente. Vedi gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html e cerca l'opzione -flto.
Peter - Ripristina Monica il

1

Regola molto generale: il compilatore può ottimizzare meglio di te. Ovviamente, ci sono eccezioni se stai facendo cose ad alta intensità di loop, ma nel complesso se vuoi una buona ottimizzazione per velocità o dimensione del codice scegli saggiamente il tuo compilatore.


Purtroppo è vero per la maggior parte dei programmatori oggi.
Dorian,

0

Dipende sicuramente dal tuo stile di codifica. Una regola generale che è là fuori è che i nomi delle variabili così come i nomi delle funzioni dovrebbero essere il più chiari e autoesplicativi possibile. Più chiamate secondarie o linee di codice vengono inserite in una funzione, più diventa difficile definire un'attività chiara per quella funzione. Nel tuo esempio hai una funzione initModule()che inizializza roba e chiama sub-routine che poi impostano l'orologio o impostano la configurazione . Puoi dirlo semplicemente leggendo il nome della funzione. Se inserisci direttamente tutto il codice dalle subroutine initModule(), diventa meno ovvio ciò che la funzione fa effettivamente. Ma come spesso, è solo una linea guida.


Grazie per la risposta. Potrei cambiare stile se necessario per le prestazioni, ma la domanda qui è: la leggibilità del codice influenza le prestazioni?
MaNyYaCk,

Una chiamata di funzione comporterà una chiamata o un comando jmp, ma secondo me questo è un sacrificio trascurabile di risorse. Se si utilizzano modelli di progettazione, a volte si finisce con una dozzina di livelli di chiamate di funzione prima di raggiungere il codice effettivo snipped.
po.pe

@Humpawumpa - Se stai scrivendo per un microcontrollore con solo 256 o 64 byte di RAM, una dozzina di strati di chiamate di funzioni non è un sacrificio trascurabile, non è proprio possibile
u

Sì, ma questi sono due estremi ... di solito hai più di 256 byte e stai usando meno di una dozzina di strati - si spera.
po.pe

0

Se una funzione fa davvero solo una cosa molto piccola, prendi in considerazione l'idea di crearla static inline.

Aggiungilo a un file di intestazione anziché al file C e usa le parole static inlineper definirlo:

static inline void setCLK()
{
    //code to set the clock
}

Ora, se la funzione è anche leggermente più lunga, come essere su 3 righe, potrebbe essere una buona idea evitare static inline e aggiungerla al file .c. Dopotutto, i sistemi integrati hanno una memoria limitata e non si desidera aumentare troppo la dimensione del codice.

Inoltre, se si definisce la funzione file1.ce la si utilizza da file2.c, il compilatore non la incorporerà automaticamente. Tuttavia, se lo definisci file1.hcome astatic inline funzione, è probabile che il tuo compilatore lo integri.

Queste static inlinefunzioni sono estremamente utili nella programmazione ad alte prestazioni. Li ho trovati per aumentare le prestazioni del codice spesso di un fattore superiore a tre.


"come essere su 3 righe" - il conteggio delle righe non ha nulla a che fare con esso; il costo di allineamento ha tutto a che fare con esso. Potrei scrivere una funzione a 20 righe che è perfetta per l'allineamento, e una funzione a 3 righe che è orribile per l'allineamento (ad es. FunctionA () che chiama functionB () 3 volte, functionB () che chiama functionC () 3 volte e un paio di altri livelli).
Jason S,

Inoltre, se si definisce la funzione file1.ce la si utilizza da file2.c, il compilatore non la incorporerà automaticamente. Falso . Vedi ad esempio -fltoin gcc o clang.
berendi - protestando contro il

0

Una difficoltà nel provare a scrivere codice efficiente e affidabile per i microcontrollori è che alcuni compilatori non possono gestire certe semantiche in modo affidabile a meno che il codice non usi direttive specifiche del compilatore o disabiliti molte ottimizzazioni.

Ad esempio, se ha un sistema single-core con una routine di servizio di interruzione [eseguita da un timer o altro]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

dovrebbe essere possibile scrivere funzioni per avviare un'operazione di scrittura in background o attendere il completamento:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

e quindi invocare tale codice usando:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Sfortunatamente, con le ottimizzazioni complete abilitate, un compilatore "intelligente" come gcc o clang deciderà che non è possibile che la prima serie di scritture possa avere alcun effetto sull'osservabile del programma e che quindi possano essere ottimizzate. Compilatori di qualità comeicc sono meno inclini a farlo se l'atto di impostare un interrupt e in attesa di completamento comporta sia scritture volatili che letture volatili (come nel caso qui), ma la piattaforma scelta come target daicc non è così popolare per i sistemi embedded.

Lo standard ignora deliberatamente i problemi di qualità dell'attuazione, ritenendo che esistano diversi modi ragionevoli per gestire il costrutto sopra riportato:

  1. Implementazioni di qualità destinate esclusivamente a settori come lo scricchiolio dei numeri di fascia alta potrebbe ragionevolmente prevedere che il codice scritto per tali campi non contenga costrutti come sopra.

  2. Un'implementazione di qualità può trattare tutti gli accessi agli volatileoggetti come se potessero innescare azioni che avrebbero accesso a qualsiasi oggetto visibile al mondo esterno.

  3. Un'implementazione semplice ma di qualità decente destinata all'uso di sistemi embedded potrebbe trattare tutte le chiamate a funzioni non contrassegnate come "inline" come se potessero accedere a qualsiasi oggetto che è stato esposto al mondo esterno, anche se non tratta volatilecome descritto in # 2.

La norma non tenta di suggerire quale degli approcci di cui sopra sarebbe più appropriato per un'implementazione di qualità, né di richiedere che implementazioni "conformi" siano di qualità sufficientemente buona da poter essere utilizzate per qualsiasi scopo particolare. Di conseguenza, alcuni compilatori come gcc o clang richiedono effettivamente che qualsiasi codice che desideri utilizzare questo modello debba essere compilato con molte ottimizzazioni disabilitate.

In alcuni casi, accertarsi che le funzioni I / O si trovino in un'unità di compilazione separata e che un compilatore non abbia altra scelta se non presumere che possano accedere a qualsiasi sottoinsieme arbitrario di oggetti che sono stati esposti al mondo esterno potrebbe essere un minimo ragionevole- of-mali modo di scrivere codice che funzionerà in modo affidabile con gcc e clang. In tali casi, tuttavia, l'obiettivo non è quello di evitare il costo aggiuntivo di una chiamata di funzione non necessaria, ma piuttosto di accettare il costo che dovrebbe essere non necessario in cambio dell'ottenimento della semantica richiesta.


"assicurarsi che le funzioni I / O siano in un'unità di compilazione separata" ... non è un modo sicuro per prevenire problemi di ottimizzazione come questi. Almeno LLVM e credo che GCC eseguirà l'ottimizzazione dell'intero programma in molti casi, quindi potremmo decidere di incorporare le tue funzioni IO anche se si trovano in un'unità di compilazione separata.
Jules il

@Jules: non tutte le implementazioni sono adatte per la scrittura di software incorporato. Disabilitare l'ottimizzazione dell'intero programma può essere il modo meno costoso per forzare gcc o clang a comportarsi come un'implementazione di qualità adatta a tale scopo.
supercat

@Jules: un'implementazione di qualità superiore destinata alla programmazione integrata o di sistemi dovrebbe essere configurabile per avere semantiche adatte a tale scopo senza dover disabilitare completamente l'ottimizzazione dell'intero programma (ad esempio avendo un'opzione per trattare gli volatileaccessi come se potessero potenzialmente innescarsi accessi arbitrari ad altri oggetti), ma per qualsiasi motivo gcc e clang preferirebbero trattare i problemi di qualità di implementazione come un invito a comportarsi in modo inutile.
supercat

1
Anche le implementazioni di "massima qualità" non correggeranno il codice buggy. Se buffnon viene dichiarato volatile, non verrà trattato come una variabile volatile, gli accessi potrebbero essere riordinati o ottimizzati completamente se apparentemente non verranno utilizzati in seguito. La regola è semplice: contrassegnare tutte le variabili a cui è possibile accedere al di fuori del normale flusso del programma (come visto dal compilatore) come volatile. Il contenuto di buffaccesso è in un gestore di interrupt? Sì. Quindi dovrebbe essere volatile.
berendi - protestando contro il

@berendi: i compilatori possono offrire garanzie oltre a quanto richiesto dallo standard e i compilatori di qualità lo faranno. Un'implementazione indipendente di qualità per l'uso di sistemi embedded consentirà ai programmatori di sintetizzare costrutti mutex, che è essenzialmente ciò che fa il codice. Quando magic_write_countè zero, l'archiviazione è di proprietà della linea principale. Quando è diverso da zero, è di proprietà del gestore di interrupt. Rendere buffvolatile richiederebbe che ogni funzione in qualsiasi luogo operi su di essa utilizzi volatilepuntatori qualificati, il che comprometterebbe l'ottimizzazione molto più che avere un compilatore ...
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.