Domanda

Quando si compila codici C / C ++ utilizzando gcc / g ++, se ignora la mia registro, può dirmi? Ad esempio, in questo codice

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    return 0;
}

j sarà utilizzato come registro, ma in questo codice

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    int * a = &j;
    return 0;
}

j sarà una variabile normale. Può dirmi se una variabile che ho usato registro è davvero memorizzato in un registro della CPU?

È stato utile?

Soluzione

È possibile ragionevolmente supporre che GCC ignora la parola chiave register tranne forse a -O0. Tuttavia, non dovrebbe fare una differenza modo uno o l'altro, e se ci si trova in tale profondità, si dovrebbe già essere la lettura del codice assembly.

Ecco un filo informativo su questo argomento: http: // gcc.gnu.org/ml/gcc/2010-05/msg00098.html. Torna ai vecchi tempi, anzi ha contribuito register compilatori di allocare una variabile in registri, ma oggi l'allocazione registro può essere realizzato in modo ottimale, automaticamente, senza suggerimenti. La parola chiave non continuare a servire a due scopi in C:

  1. In C, vi impedisce di prendere l'indirizzo di una variabile. Dal momento che i registri non hanno indirizzi, questa limitazione può aiutare un semplice compilatore C. (Non esistono semplice compilatori C ++.)
  2. Un oggetto register non può essere dichiarato restrict. Perché riguarda restrict a indirizzi, la loro intersezione è inutile. (C ++ non ha ancora restrict, e in ogni caso, questa regola è un po 'banale.)

Per C ++, la parola chiave è stata sconsigliata dal C ++ 11 e proposto per rimozione dal di revisione standard prevista per il 2017.

Alcuni compilatori hanno utilizzato register sulle dichiarazioni di parametri per determinare la convenzione di chiamata di funzioni, con l'ABI che permette stack- misto e parametri basati sui registri. Questo sembra essere non conforme, tende a verificarsi con sintassi estesa come register("A1"), e non so se tale compilatore è ancora in uso.

Altri suggerimenti

Per quanto riguarda le moderne tecniche di compilazione e di ottimizzazione, l'annotazione register non ha alcun senso a tutti. Nel suo secondo programma si prende l'indirizzo del j e registri non hanno indirizzi, ma una stessa variabile locale o statica potrebbe benissimo essere memorizzati in due diverse posizioni di memoria durante la sua vita, o talvolta in memoria e talvolta in un registro, o non esiste affatto. In effetti, un compilatore ottimizzato sarebbe compilare i cicli annidati come nulla, perché non hanno alcun effetto, e semplicemente assegnare i loro valori finali per k e j. E poi omettere questi incarichi perché il codice rimanente non fa uso di questi valori.

Non è possibile ottenere l'indirizzo di un registro in C, più il compilatore può totalmente ignorarti; standard di C99, sezione 6.7.1 (pdf) :

  

L'applicazione può trattare qualsiasi   registrare la dichiarazione semplicemente come un auto   dichiarazione. Tuttavia, anche se non   memorizzazione indirizzabile è effettivamente utilizzato,   l'indirizzo di una parte di un oggetto   dichiarato con classe di archiviazione specificatore   registro non può essere calcolato, sia   esplicitamente (con l'uso del unario &   operatore come discusso in 6.5.3.2) o   implicitamente (convertendo un array   nome di un puntatore come discusso in   6.3.2.1). Pertanto, l'unico operatore che può essere applicata a un array dichiarati   con classe di archiviazione registro specificatore   è sizeof.

A meno che non si sta gingillarsi su AVR 8-bit o PIC, il compilatore probabilmente ridere di voi pensando si conosce meglio e ignorare i motivi. Anche su di loro, ho pensato che ho conosciuto meglio un paio di volte e modi trovati per ingannare il compilatore (con un po 'asm inline), ma il mio codice esplosa perché doveva massaggiare una serie di altri dati per aggirare la mia testardaggine.

Questa domanda, e alcune delle risposte, e diverse altre discussioni delle parole chiave 'Registrati' che ho visto - sembra assumere implicitamente che tutti i locali sono mappati o ad un registro specifico, o ad una locazione di memoria specifica su la pila. Questo è stato vero in generale fino a 15-25 anni fa, ed è vero se si spegne l'ottimizzazione, ma non è vero niente quando viene eseguita ottimizzazione standard. Gli abitanti del posto sono ora visti da ottimizzatori come nomi simbolici che si utilizza per descrivere il flusso di dati, piuttosto che come valori che devono essere conservati in luoghi specifici.

Nota: da 'locali' qui voglio dire: variabili scalari, della classe di memorizzazione automatica (o 'registro'), che non vengono mai utilizzati come operando di '&'. I compilatori a volte possono rompere le strutture auto, sindacati o array in singole variabili 'locali', anche.

Per illustrare questo: supponiamo che io scrivo questo nella parte superiore di una funzione:

int factor = 8;

.. e poi il solo uso della variabile factor è quello di moltiplicare per varie cose:

arr[i + factor*j] = arr[i - factor*k];

In questo caso - provare se si vuole - non ci sarà variabile factor. L'analisi del codice mostrerà che è sempre factor 8, e così tutti i turni si trasformerà in <<3. Se avete fatto la stessa cosa nel 1985 C, factor otterrebbe una posizione sullo stack, e non ci sarebbe multipilies, dal momento che i compilatori fondamentalmente lavorato un'istruzione alla volta e di non ricordare nulla circa i valori delle variabili. A quei tempi i programmatori sarebbero più propensi a utilizzare #define factor 8 per ottenere un codice migliore in questa situazione, pur mantenendo factor regolabile.

Se si utilizza -O0 (ottimizzazione off) - si sarà davvero ottenere una variabile per factor. Questo vi permetterà, per esempio, di scavalcare la dichiarazione factor=8, e quindi il cambiamento factor a 11 con il debugger, e andare avanti. Affinché ciò funzioni, il compilatore non può tenere il niente nei registri tra dichiarazioni, fatta eccezione per le variabili che vengono assegnati ai registri specifici; e in tal caso il debugger è informato di questo. E non può cercare di 'sapere' nulla circa i valori delle variabili, dal momento che il debugger di loro potrebbe cambiare. In altre parole, è necessario la situazione 1985, se si desidera modificare le variabili locali durante il debug.

compilatori moderni compilare generalmente una funzione come segue:

(1) quando una variabile locale viene assegnato a più di una volta in una funzione, il compilatore crea diverse 'versioni' della variabile in modo che ciascuno di essi viene assegnato solo in un posto. Tutte le 'letture' della variabile si riferiscono a una versione specifica.

(2) Ognuno di questi locali è assegnato a un registro 'virtuale'. Intermedi risultati dei calcoli vengono anche assegnati variabili / registri; così

  a = b*c + 2*k;

diventa qualcosa di simile

       t1 = b*c;
       t2 = 2;
       t3 = k*t2;
       a = t1 + t3;

(3) Il compilatore prende allora tutte queste operazioni, e cerca sottoespressioni comuni, ecc Dal momento che ciascuno dei nuovi registri è sempre e solo scritto una volta, è piuttosto facile per riordinarli, pur mantenendo la correttezza. Non voglio nemmeno iniziare l'analisi del ciclo.

(4) Il compilatore tenta quindi di mappare tutti questi registri virtuali nei registri reali al fine di generare il codice. Poiché ciascun registro virtuale ha una durata limitata è possibile riutilizzare registri reali pesantemente - 't1' in quanto sopra è necessaria solo fino aggiuntivo che genera 'un', in modo che possa essere tenuto nello stesso registro come 'un'. Quando non ci sono abbastanza registri, alcuni dei registri virtuali possono essere assegnate a memoria - o - un valore può essere tenuto in una certa registro memorizzato nella memoria per un po ', e ritorna caricati in un (eventualmente) registro differente dopo . Su una macchina load-store, dove solo valori nei registri possono essere utilizzati nei calcoli, questa seconda strategia accoglie che bene.

Da quanto sopra, questo dovrebbe essere chiaro: è faciledeterminare che il registro virtuale mappato factor è la stessa come la costante '8', e quindi tutti moltiplicazioni per factor sono moltiplicazioni per 8. Anche se factor viene modificata successivamente, che è una variabile 'nuovo' e non influisce prima usi di factor.

Un'altra implicazione, se si scrive

 vara = varb;

.. si può o non può essere il caso che ci sia una copia corrispondente nel codice. Per esempio

int *resultp= ...
int acc = arr[0] + arr[1];
int acc0 = acc;    // save this for later
int more = func(resultp,3)+ func(resultp,-3);
acc += more;         // add some more stuff
if( ...){
    resultp = getptr();
    resultp[0] = acc0;
    resultp[1] = acc;
}

Nel sopra i due 'versioni' ACC (iniziale, e dopo l'aggiunta 'più') potrebbe essere in due registri differenti, e 'acc0' sarebbe allora lo stesso del inital 'acc'. Quindi nessuna copia registro sarebbe necessaria per 'acc0 = acc'. Un altro punto:. Il 'resultp' è assegnato due volte, e poiché la seconda assegnazione ignora il valore precedente, ci sono essenzialmente due variabili distinte 'resultp' nel codice, e questo è facilmente determinata mediante analisi

Un'implicazione di tutto questo: non essere riluttanti a uscire espressioni complesse in quelle più piccole che utilizzano i locali aggiuntivi per gli intermedi, se rende il codice più facile da seguire. Non v'è praticamente pari a zero penalità di run-time per questo, dal momento che l'ottimizzatore vede la stessa cosa in ogni caso.

Se siete interessati a saperne di più si potrebbe cominciare da qui: http://en.wikipedia.org / wiki / Static_single_assignment_form

Il punto di questa risposta è di (a) danno un'idea di come i compilatori moderni lavoro e (b) sottolineano che chiedere il compilatore, se sarebbe così gentile, per mettere una particolare variabile locale in un registro - in realtà non ha senso. Ogni 'variabile' può essere visto da l'ottimizzatore di diverse variabili, alcune delle quali possono essere fortemente utilizzati in loop, e altri no. Alcune variabili svaniranno - per esempio essendo costante; o, talvolta, la variabile temperatura utilizzata in uno scambio. O calcoli non effettivamente utilizzati. Il compilatore è in grado di utilizzare lo stesso registro per cose diverse in diverse parti del codice del, secondo ciò che è in realtà meglio sulla macchina che si sta compilando per.

La nozione di sintomatiche compilatore su quale variabili dovrebbero essere in registri presuppone che ciascuna variabile locale associato a un registro o ad una locazione di memoria. Questo era vero indietro quando Kernighan + Ritchie ha progettato il linguaggio C, ma non è più vero.

Per quanto riguarda la limitazione che non si può prendere l'indirizzo di una variabile registro: Chiaramente, non c'è modo per implementare prendere l'indirizzo di una variabile tenuta in un registro, ma si potrebbe chiedere - dal momento che il compilatore ha facoltà di ignorare il 'registrati' - perché è questa regola a posto? Perché non è possibile che il compilatore semplicemente ignorare il 'registro' se mi capita di prendere l'indirizzo? (Come nel caso in C ++).

Anche in questo caso, si deve tornare indietro al vecchio compilatore. Il compilatore originale K + R sarebbe analizzare una dichiarazione variabile locale, e quindi immediatamente decidere se assegnare ad un registro o meno (e, se così, che registrano). Poi sarebbe procedere ad espressioni di compilazione, emettendo l'assemblatore per ogni istruzione, uno alla volta. Se in seguito è emerso che si stavano prendendo l'indirizzo di una variabile 'registro', che era stato assegnato a un registro, non c'era modo di gestire che, dal momento che l'assegnazione è stata, in generale, irreversibili per allora. E 'stato possibile, tuttavia, per generare un messaggio di errore e la fermata compilazione.

In conclusione, sembra che 'registrare' è essenzialmente obsoleto:

  • compilatori C ++ ignorano completamente
  • compilatori
  • C ignorano salvo che in esecuzione la restrizione sulla & - e forse non lo ignorano a -O0 dove potrebbe effettivamente portare a ripartizione, come richiesto. A -O0 non sono preoccupati per la velocità di codice però.

Quindi, è fondamentalmente c'è ora la compatibilità a ritroso, e, probabilmente, sulla base del fatto che alcune implementazioni Could essere ancora di utilizzarlo per 'suggerimenti'. Non ho mai lo uso - e scrivo il codice DSP in tempo reale, e trascorrere un bel po 'di tempo a guardare il codice generato e di trovare modi per renderlo più veloce. Ci sono molti modi per modificare il codice per farlo funzionare più velocemente, e sapendo come i compilatori lavoro è molto utile. E 'passato molto tempo dall'ultima volta che ho davvero scoperto che l'aggiunta di 'registrare' di essere tra quei modi.


Addendum

I esclusi sopra, dal mio speciale definizione di 'locali', variabili a cui si applica & (questi sono sono ovviamente inclusi nel senso usuale del termine).

Si consideri il seguente codice:

void
somefunc()
{
    int h,w;
    int i,j;
    extern int pitch;

    get_hw( &h,&w );  // get shape of array

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*pitch + j] = generate_func(i,j);
        }
    }
}

Questo può sembrare perfettamente innocua. Ma se siete preoccupati per la velocità di esecuzione, considerare questo: Il compilatore sta passando gli indirizzi dei h e w a get_hw, e poi chiamando generate_func. Supponiamo che il compilatore non sa nulla di ciò che è in quelle funzioni (che è il caso generale). Il compilatore deve per scontato che la chiamata a generate_func potrebbe cambiare h o w. Questo è un uso perfettamente legale del puntatore passato a get_hw -. È possibile memorizzare da qualche parte e quindi utilizzare in un secondo momento, a patto che il campo di applicazione contenente h,w è ancora in gioco, di leggere o scrivere quelle variabili

In questo modo il compilatore deve negozio h e w in memoria sullo stack, e non può determinare nulla in anticipo su quanto tempo il ciclo verrà eseguito. Così alcune ottimizzazioni sarà impossibile, e il ciclo potrebbe essere meno efficiente come risultato (in questo esempio, c'è una chiamata di funzione nel ciclo interno in ogni caso, in modo che non può fare molta differenza, ma prendere in considerazione il caso in cui ci sia una funzione che è occasionalmente chiamato nel ciclo interno, a seconda qualche condizione).

Un altro problema qui è che generate_func potrebbe cambiare pitch, e così le esigenze i*pitch a fare ogni volta, e non solo quando le modifiche i.

Può essere ricodificato come:

void
somefunc()
{
    int h0,w0;
    int h,w;
    int i,j;
    extern int pitch;
    int apit = pitch;

    get_hw( &h0,&w0 );  // get shape of array
    h= h0;
    w= w0;

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*apit + j] = generate_func(i,j);
        }
    }
}

Ora le variabili apit,h,w sono tutti i locali 'sicuri' nel senso che ho definito sopra, e il compilatore può essere sicuro che non verrà modificata da eventuali chiamate di funzione. Supponendo che io sono non modifica nulla nella generate_func, il codice avrà lo stesso effetto di prima, ma potrebbe essere più efficiente.

Jens Gustedt ha suggerito l'uso della parola chiave 'registrare' come un modo di codifica variabili chiave per inibire l'uso di & su di loro, per esempio da altri mantenendo il codice (non influenzerà il codice generato, poiché il compilatore può determinare la mancanza di & senza). Da parte mia, ho sempre riflettere attentamente prima di applicare & a qualsiasi scalare locale in una zona time-critical del codice, e secondo me con 'registro' per far rispettare questo è un po 'criptico, ma posso vedere il punto (purtroppo non funziona in C ++ in quanto il compilatore semplicemente ignorare il 'registro').

Per inciso, in termini di efficienza del codice, il modo migliore per avere un rendimento funzione due valori è con una struttura:

struct hw {  // this is what get_hw returns
   int h,w;
};

void
somefunc()
{
    int h,w;
    int i,j;

    struct hw hwval = get_hw();  // get shape of array
    h = hwval.h;
    w = hwval.w;
    ...

Questo può sembrare ingombrante (ed è ingombrante per scrivere), ma genererà il codice più pulito rispetto agli esempi precedenti. Il 'struct hw' saranno effettivamente restituiti in due registri (sulla maggior parte ABI moderni in ogni caso). E a causa del modo in cui il 'hwval' struct viene utilizzato, l'ottimizzatore efficace suddividerlo in hwval.h e hwval.w due 'locali', e quindi determinare che questi sono equivalenti a h e w - così hwval sarà essenzialmente scompare nel codice . Non ci sono puntatori necessitano di essere passati, nessuna funzione sta modificando le variabili di un'altra funzione tramite puntatore; è come avere due valori di ritorno scalari distinti. Questo è molto più facile da fareora in C ++ 11 -. con std::tie e std::tuple, è possibile utilizzare questo metodo con meno di dettaglio (e senza dover scrivere una definizione struct)

Il secondo esempio è valido in C. Così si vede bene che la parola chiave register cambia qualcosa (in C). È solo lì per questo scopo, per inibire la presa di un indirizzo di una variabile. Quindi, solo non prendere il suo nome "register" verbalmente, è un termine improprio, ma bastone per la sua definizione.

Che C ++ sembra ignorare register, così devono avere la loro ragione di ciò, ma lo trovo un po 'triste di ritrovare uno di questi sottile differenza in cui il codice valido per un non è valida per l'altro.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top