2015-07-02 14 views
9

Sto lavorando a un progetto C++/Qt per Embedded Linux in cui siamo costantemente "impegnati" contro le limitazioni del nostro processore, specialmente quando si tratta di aggiornare i grafici nell'interfaccia utente. Grazie a queste limitazioni (e specialmente alla nostra situazione qualche tempo fa, quando le cose andavano ancora peggio), cerco di ottimizzare il codice sempre quando posso e se i costi di ottimizzazione sono minimi. Una delle ottimizzazioni che stavo facendo è quella di utilizzare sempre il valore intero corretto per la situazione che sto gestendo: qint8, qint16 e qint32 a seconda di quanto è grande il valore di cui ho bisogno.Dovrei preferire usare piccoli tipi di int (int8 e int16) in codice C++?

Ma qualche tempo fa ho letto da qualche parte che, invece di provare a utilizzare la dimensione minima del numero intero quando possibile, dovrei sempre preferire usare il valore intero relativo alla capacità del mio processore, cioè se il mio processore è 32 -bit oriented, quindi preferirei usare qint32 sempre anche quando non era richiesto un numero così grande. In un primo momento non sono riuscito a capire perché, ma suggerisco the answer to this question perché le prestazioni del processore sono maggiori quando deve funzionare con la sua "dimensione predefinita di numero intero".

Beh, non ne sono convinto. Prima di tutto non è stato fornito alcun riferimento effettivo a conferma di una tesi di questo tipo: non riesco a capire perché scrivere e leggere da uno spazio di memoria a 32 bit sarebbe più lento, quindi farlo con un numero intero a 32 bit (e la spiegazione fornita non era molto comprensibile , btw). In secondo luogo ci sono alcuni momenti nella mia app quando ho bisogno di trasferire dati da una parte all'altra, come quando utilizzo il meccanismo di segnali e slot di Qt. Dal momento che sto trasferendo i dati da un punto all'altro, i dati più piccoli non danno sempre un miglioramento rispetto ai dati più grandi? Intendo dire che un segnale che invia due caratteri (non per riferimento) non dovrebbe eseguire il lavoro più rapidamente, quindi inviare due interi a 32 bit?

Infatti, mentre la "spiegazione del processore" suggerisce di utilizzare le caratteristiche del processore, altri casi suggeriscono il contrario. Ad esempio, quando si ha a che fare con database, le conversioni this e this suggeriscono entrambi che vi è un vantaggio (anche se solo in alcuni casi) nell'utilizzo di versioni più piccole di numero intero.

Quindi, dopo tutto, dovrei preferire usare piccoli tipi di int quando il contesto lo consente o no? O esiste un elenco di casi in cui un approccio o l'altro è più propenso a fornire risultati migliori o peggiori? (Ad esempio, dovrei usare int8 e int16 quando uso i database, ma il tipo predefinito del mio processore in tutte le altre situazioni)

E come ultima domanda: Qt normalmente ha implementazioni int-based delle sue funzioni. In tali casi, l'operazione di cast non annulla ogni possibile miglioramento che si potrebbe avere usando gli interi minori?

+0

AFAIK a seconda del processore, qualsiasi cosa più piccola di int è implicitamente trasmessa a int per eseguire operazioni. Comunque uso sempre il tipo giusto per mostrare ad altre persone quale sia la mia intenzione. – Robinson

+0

Bjarne Stroustrup preferisce usare un int a meno che non si abbia bisogno di qualcosa di più o di bisogno di unsigned. – NathanOliver

+0

Ha ragione, ma a volte in realtà vuoi che int8_t [4] abbia una dimensione di 4 byte, non 16. – Robinson

risposta

-1

Per informazioni sulla promozione integrale:

http://en.cppreference.com/w/cpp/language/implicit_cast

Prvalues ​​di piccoli tipi integrali (come char) possono essere convertiti in prvalues ​​di tipi integrali più grandi (come int). In particolare, gli operatori aritmetici non accettano tipi più piccoli di int come argomenti e le promozioni integrali vengono applicate automaticamente dopo la conversione da lvalue-to-rvalue, se applicabile. Questa conversione sempre conserva il valore.

1

Il concetto di utilizzare sempre int è buono per i valori temporanei (come una variante anello), in generale, perché è probabile di essere promosso a int per molte operazioni o chiamate di libreria.

Quando si tratta di archiviare grandi quantità di dati, specialmente negli array, quindi usare un tipo più piccolo è molto meglio. La domanda è, quanti sono grandi, ed è sfortunatamente situazionale.

L'imbottitura della struttura offre anche un po 'di spazio di manovra su quando è possibile utilizzare gratuitamente uno int completo. Ad esempio, se ci sono 3 short s, il più utilizzato potrebbe essere un int. Per inciso, è necessario ordinare i membri in base alle dimensioni per evitare spazi non necessari a causa del riempimento.

La sfortunata risposta, soprattutto se si sta utilizzando un ambiente con risorse limitate come Embedded Linux, è la verifica. A volte varrà la pena lo spazio, a volte non lo farà.

+0

Il riempimento, ecc. Dipende dall'ABI e dal PC per la piattaforma. Potrebbe anche essere applicato tra due membri 'char' (ma non tra i campi dell'array). – Olaf

3

Un argomento valido contro l'utilizzo di piccole variabili è che quando si esegue il mapping ai registri (supponendo che non siano espansi implicitamente), possono causare dipendenze false non intenzionali se il proprio ISA utilizza registri parziali. Questo è il caso di x86, poiché alcuni vecchi programmi utilizzano ancora AH o AX e le loro controparti come registri di dimensioni 8/16 bit. Se il tuo registro ha qualche valore bloccato nella parte superiore (a causa di una precedente scrittura nel registro completo), la tua CPU potrebbe essere costretta a portarlo avanti e fonderlo con qualsiasi valore parziale calcolato per mantenere la correttezza, causando catene seriali di dipendenze anche se i tuoi calcoli erano indipendenti.

Anche la richiesta di memoria sollevata dalla risposta che hai collegato, anche se la trovo un po 'più debole, è vero che i sottosistemi di memoria di solito funzionano con la granularità completa della cache (che è spesso 64 byte al giorno), quindi ruotano e maschera, ma questo da solo non dovrebbe causare un impatto sulle prestazioni - semmai migliora le prestazioni quando i tuoi pattern di accesso ai dati mostrano località spaziali. In alcuni casi, variabili più piccole possono anche aumentare il rischio di causare problemi di allineamento, soprattutto se si comprimono da vicino variabili di dimensioni diverse, ma la maggior parte dei compilatori dovrebbe conoscere meglio (a meno che non sia esplicitamente obbligato a non farlo).

Penso che il problema principale con le piccole variabili sulla memoria, sarebbe di nuovo - aumentando le possibilità di false dipendenze - la fusione viene eseguita implicitamente dal sistema di memoria, ma se altri core (o socket) condividono alcune di voi variabili , corri il rischio di far saltare tutta la linea fuori dalla cache)

1

In generale, l'ottimizzazione troppo precoce è poco utile. Per le variabili locali e le classi e le strutture più piccole, c'è poco o nessun guadagno nell'usare tipi non nativi. A seconda della chiamata standard della procedura, l'impacchettamento/decompressione di tipi più piccoli in un unico registro potrebbe persino aggiungere più codice rispetto al costo dei tipi di parole.

Per matrici più grandi, nodi elenco/struttura (IOW: strutture dati più grandi), tuttavia, le cose possono essere diverse. Potrebbe valere la pena qui usare i tipi appropriati, non il naturale, usare le strutture C-stype senza metodi, ecc. Per la maggior parte della "architettura moderna" (dalla fine del secolo scorso) compatibile con Linux, non c'è quasi nessuna penalità per i più piccoli tipi interi. Per i tipi float, potrebbero esserci architetture che supportano il float, non il doppio dell'hardware o il doppio dell'elaborazione. Per questi, usare il tipo più piccolo non solo riduce l'ingombro della memoria, ma è anche più veloce.

Invece di ridurre i tipi di membri/variabili, vale la pena ottimizzare la gerarchia delle classi o persino utilizzare il codice C nativo (o la codifica in stile C) per alcune parti. Cose come i metodi virtuali o RTTI possono essere piuttosto costose. Il primo utilizza grandi tabelle di salto, il secondo aggiunge descrittori per ogni classe.

Si noti che alcune istruzioni presuppongono che codice e dati risiedano nella RAM come tipici per i sistemi Linux (incorporati). Se codice/costanti sono memorizzati in Flash, ad es., È necessario ordinare le istruzioni in base all'impatto sul rispettivo tipo di memoria.

1

Dipende da ciò che si sta facendo, se ogni elemento passa attraverso alcuni calcoli matematici, quindi a seconda del set di istruzioni potrebbe dover mascherare e firmare estendere qualsiasi cosa più piccola del registro, dove se si abbina la dimensione del registro allora si non avremo quel problema e quel codice extra generato tutto il tempo. Suppongo che sia per questo che int o uno qualsiasi dei tipi di variabili standard C/C++ siano indipendenti dalla dimensione, usarli per questo motivo.

C'è anche il problema degli allineamenti, i processori più recenti sono migliori, ma non importa quale sia se non si è allineati al limite del bus del ram o del bus del processore si possono/si bruciano cicli di clock aggiuntivi anche se si abbina la dimensione del processore. Inoltre, a seconda del processore e di tutta la logica del controllore che si estende da esso, gli accessi a più byte all'interno della stessa area possono comportare cicli di bus separati, anche se il processore può firmare estensioni o zero estensioni per l'utente all'interno dell'istruzione. E naturalmente alcuni processori stessi o i bus che si estendono potrebbero ottimizzare, basta leggere quella parola, ecco un byte da esso, e poi si colpisce la cache se ne hai uno quindi un singolo byte letto può/risulterà in più parole essere recuperati e memorizzati, quindi anche se il processore richiede più cicli, sono più brevi e più veloci. Piccoli sforzi per mantenere le cose allineate su 4 o 8 o più grandi limiti di byte aiutano le prestazioni su quasi tutte le piattaforme (se si spostano i dati in blocchi di queste dimensioni o più grandi dalla prospettiva del codice di alto livello)

Per i BLOB di dati, è possibile che semplicemente spostarsi e non lavorare tanto quanto i singoli byte, quindi masterizzare 32 bit per memorizzare 8 o 16 è ovviamente un enorme spreco di ram, che spreca cache se ne hai uno, e le prestazioni diminuiscono perché lo spazio della cache potrebbe essere usato per altre cose o tenere le stesse cose più a lungo.

Sei nel regno dell'ottimizzazione prematura. Puoi fare alcune cose come questa in genere evitando di sprecare alcune istruzioni qua e là (usa intigned unsigned o int per variabili di loop per esempio e lascia che il compilatore scelga le dimensioni, costruisci le strutture con gli elementi allineati/più grandi prima poi più piccoli ultimi articoli), ma ottenere il codice funzionante e debug, isolare i problemi di prestazioni, se del caso, quindi pesare l'ottimizzazione di una piattaforma/compilatore vs portabilità e manutenibilità. O semplicemente flat out mantenere una versione linguistica di alto livello di una funzione e avere una versione di assembly sintonizzata a mano alternativa per una piattaforma specifica, in modo da poter tornare indietro (può anche solo compilare quindi modificare/correggere l'asm). Ma di nuovo solo se davvero hai davvero bisogno di prestazioni extra e hai isolato una sezione a basso rendimento e sei disposto a pagarne il prezzo.

4

Questa domanda è davvero troppo ampia senza specificare una CPU specifica. Poiché alcune CPU a 32 bit dispongono di numerose istruzioni per la gestione di tipi più piccoli, altre no. Alcune CPU a 32 bit gestiscono l'accesso disallineato, alcuni producono codice più lento a causa di esso e alcuni si fermano e prendono fuoco quando lo incontrano.


Detto questo, prima di tutto c'è il caso di promozione intero di serie su tutti i programmi C e C++, che implicitamente convertire tutti i tipi interi piccoli che si usa in int.

Il compilatore è libero di utilizzare la promozione di interi come specificato nello standard o di ottimizzarlo, a seconda di quale conduce al codice più efficace, purché i risultati siano gli stessi del codice non ottimizzato.

La promozione implicita può creare codice più efficace ma può anche creare bug sottili e disastrosi con modifiche impreviste del tipo e della firma, se il programmatore non è a conoscenza di come funzionano le varie regole di promozione del tipo implicito. Purtroppo, molti programmatori C e C++ non lo sono. Quando si utilizzano tipi interi più piccoli, è necessario essere un programmatore molto più competente/sveglio rispetto a quando si utilizzano solo variabili a 32 bit dappertutto.

Quindi, se stai leggendo questo, ma non avete mai sentito parlare di le regole di promozione intero o le solite conversioni aritmetiche/bilanciamento, allora vi consiglierei vivamente di interrompere immediatamente qualsiasi tentativo di ottimizzare manualmente formati interi e vai su read up invece su quelle regole di promozione implicite.


Se si è a conoscenza di tutte le regole di promozione implicite, è possibile eseguire l'ottimizzazione manuale utilizzando tipi di numeri interi più piccoli. Ma usa quelli che danno al compilatore la massima flessibilità. Questi sono:

#include <stdint.h> 

int_fast8_t 
int_fast16_t 
uint_fast8_t 
uint_fast16_t 

Quando si utilizzano questi tipi, il compilatore è libero di cambiare loro per un tipo più grande se questo sarebbe cedere codice più veloce.

La differenza tra le suddette variabili basandosi solo sull'integrazione di promozione/espressione intera è che con i tipi veloci il compilatore non può solo decidere quale tipo si adatta meglio ai registri della CPU per un dato calcolo, ma anche a decidere il consumo e l'allineamento della memoria quando le variabili sono allocate.

+0

+1 per suggerire i tipi * _fast * in . Usando questi ti darò la migliore portabilità ad altre piattaforme e il codice più veloce pure. – semaj

Problemi correlati