Domanda

Questa domanda e la mia risposta qui sotto sono principalmente in risposta ad una zona di confusione in un'altra domanda.

Al termine della risposta, ci sono alcuni problemi WRT "volatile" e la sincronizzazione dei thread che io non sono del tutto sicuri circa - Accolgo con favore i commenti e le risposte alternative. Il punto della questione riguarda principalmente registri della CPU e come vengono utilizzati, tuttavia.

È stato utile?

Soluzione

registri CPU sono piccole aree di memorizzazione dei dati sul silicio della CPU. Per la maggior parte delle architetture, sono il luogo primario di tutte le operazioni avvengono (dati vengono caricati in dalla memoria, operato, e spinto indietro).

filo

Qualunque cosa è in esecuzione utilizza i registri e possiede il puntatore all'istruzione (che dice che l'istruzione viene dopo). Quando gli swap del sistema operativo in un altro thread, tutto lo stato della CPU, inclusi i registri e il puntatore all'istruzione, vengono salvati da qualche parte, in modo efficace liofilizzazione stato del filo quando si tratta successiva torna alla vita.

Un sacco di documentazione più su tutto questo, naturalmente, tutto il luogo. Wikipedia sui registri. Wikipedia sul cambio di contesto. per cominciare. Edit: o leggere la risposta di Steve314. :)

Altri suggerimenti

I registri sono la "memoria di lavoro" in una CPU. Essi sono molto veloci, ma una risorsa molto limitata. Tipicamente, una CPU ha un piccolo insieme fisso di registri di nome, i nomi facenti parte della convenzione linguaggio assemblatore per tale codice macchina CPU. Ad esempio, 32 bit Intel CPU 86 ha quattro principali registri di dati denominati eax, ebx, ecx e edx, insieme ad un numero di indicizzazione e altri registri più specializzati.

A rigor di termini, questo non è del tutto vero in questi giorni - registrazione rinomina, per esempio, è comune. Alcuni processori hanno abbastanza registri che il censimento, piuttosto che dar loro un nome ecc Resta, tuttavia, un buon modello di base su cui lavorare. Ad esempio, register renaming viene utilizzato per mantenere l'illusione di questo modello di base nonostante esecuzione out-of-order.

Uso dei registri in assemblatore manualmente scritto tende ad avere un semplice modello di utilizzo registro. Alcune variabili saranno mantenute esclusivamente nei registri per la durata di una subroutine, o una parte sostanziale di esso. Altri registri vengono utilizzati in un modello di lettura-modifica-scrittura. Per esempio ...

mov eax, [var1]
add eax, [var2]
mov [var1], eax

IIRC, che è valida (anche se probabilmente inefficiente) codice x86 assembler. Su un Motorola 68000, potrei scrivere ...

move.l [var1], d0
add.l  [var2], d0
move.l d0, [var1]

Questa volta, la fonte è di solito il parametro a sinistra, con la destinazione a destra. 68000 aveva 8 registri dati (d0..d7) e 8 registri di indirizzi (a0..a7), con a7 serve IIRC anche come stack pointer.

In una 6510 (di nuovo sul buon vecchio Commodore 64) potrei scrivere ...

lda    var1
adc    var2
sta    var1

I registri qui sono per lo più implicita nelle istruzioni - prima di tutto l'uso la A (accumulatore) registrare il

.

Si prega di perdonare eventuali errori sciocchi in questi esempi - non ho scritto una quantità significativa di assembler "reale" (piuttosto che virtuale) per almeno 15 anni. Il principio è il punto, però.

Utilizzo di registri è specifico per un particolare frammento di codice. Che registro contiene è praticamente qualunque sia l'ultima istruzione ha lasciato in esso. E 'la responsabilità del programmatore per tenere traccia di ciò che è in ciascun registro in ogni punto del codice.

Quando si chiama una subroutine, sia il chiamante o il chiamato deve assumersi la responsabilità di garantire non c'è un conflitto, che in genere significa che i registri vengono salvati fuori allo stack all'inizio della chiamata e poi leggere di nuovo in alla fine. Problemi simili si verificano con gli interrupt. Cose come chi è responsabile per il salvataggio dei registri (chiamante o callee) sono in genere una parte della documentazione di ogni subroutine.

Un compilatore in genere decidere come utilizzare i registri in un modo molto più sofisticato di un programmatore umano, ma opera sugli stessi principi. La mappatura dei registri a particolari variabili è dinamica e varia notevolmente in base al quale frammento di codice che si sta guardando. Salvataggio e ripristino dei registri è in gran parte gestita secondo le convenzioni standard, anche se il compilatore può improvvisare "telefonica personalizzata convenzioni" in alcune circostanze.

In genere, le variabili locali in una funzione sono immaginati di vivere in pila. Questa è la regola generale con le variabili "Auto" C. Dal momento che "auto" è l'impostazione predefinita, queste sono variabili locali normali. Per esempio ...

void myfunc ()
{
  int i;  //  normal (auto) local variable
  //...
  nested_call ();
  //...
}

Nel codice di cui sopra, "i" può ben essere tenuto in primo luogo in un registro. Può anche essere spostato da un registro all'altro e indietro come i progressi funzione. Tuttavia, quando "nested_call" è chiamato, il valore da tale registro sarà quasi certamente sullo stack - o perché la variabile è una variabile di stack (non un registro), o perché il contenuto del registro vengono salvati per consentire nested_call la propria memoria di lavoro .

In un'applicazione multithreading, le variabili locali normali sono locali rispetto a un particolare thread. Ogni thread prende il proprio stack, e mentre è in funzione, l'uso esclusivo dei registri CPU. In un cambio di contesto, questi registri vengono salvati. Se ion registri o nello stack, variabili locali non sono condivisi tra fili.

Questa situazione di base è conservato in un'applicazione multicore, anche se due o più fili possono essere attivi contemporaneamente. Ogni core ha il proprio stack e propri registri.

I dati memorizzati nella memoria condivisa richiede più attenzione. Questo include variabili globali, variabili statiche all'interno di entrambi classi e funzioni e oggetti heap allocati. Per esempio ...

void myfunc ()
{
  static int i;  //  static variable
  //...
  nested_call ();
  //...
}

In questo caso, il valore di "i" è conservata tra chiamate di funzione. Una regione statica di memoria è riservata per memorizzare questo valore (da qui il nome "statico"). In linea di principio, non c'è bisogno di alcuna azione speciale per preservare "i" durante la chiamata a "nested_call", a prima vista, la variabile si può accedere da qualsiasi thread in esecuzione su qualsiasi nucleo (o anche su una CPU separata).

Tuttavia, il compilatore sta ancora lavorando duramente per ottimizzare la velocità e le dimensioni del codice. Ripetuta legge e scrive la memoria principale sono molto più lento di accessi di registro. Il compilatore sarà quasi certamente scegliere non per seguire la semplice lettura-modifica-scrittura modello sopra descritto, ma sarà invece mantenere il valore nel registro per un periodo relativamente prolungato, evitando ripetute legge e scrive la stessa la memoria.

Questo significa che le modifiche apportate a un thread non possono essere visti da un altro filo per qualche tempo. Due le discussioni potrebbero finire per avere idee molto diverse circa il valore di "i" di cui sopra.

Non esiste una soluzione magica hardware per questo. Ad esempio, non v'è alcun meccanismo per sincronizzare il registro tra i thread. Per la CPU, la variabile e il registro sono completamente entità separate - non sa che hanno bisogno di essere sincronizzati. C'è sicuramente alcuna sincronizzazione tra registri diversi filettature o in esecuzione su differenti core -. Non c'è ragione di credere che un altro thread sta utilizzando stesso registro per lo stesso scopo l'in qualsiasi momento particolare

Una soluzione parziale è a bandiera una variabile come "volatile" ...

void myfunc ()
{
  volatile static int i;
  //...
  nested_call ();
  //...
}

Questo dice al compilatore di non ottimizzare legge e scrive la variabile. Il processore non ha un concetto di volatilità. Questa parola chiave indica al compilatore di generare codice diverso, facendo immediata lettura e scrittura alla memoria come specificato da assegnazioni, invece di evitare quei accessi utilizzando un registro.

Questo è non una soluzione di sincronizzazione multithreading, tuttavia - almeno non in sé. Una soluzione multithreading appropriata è quella di utilizzare un qualche tipo di blocco per gestire l'accesso a questa "risorsa condivisa". Per esempio ...

void myfunc ()
{
  static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

Non è più succedendo qui che è immediatamente evidente. In linea di principio, piuttosto che scrivere il valore di "i" di nuovo a sua variabile pronto per la chiamata "release_lock_on_i", potrebbe essere salvato in pila. Per quanto riguarda il compilatore è interessato, questo non è irragionevole. Sta facendo l'accesso pila comunque (per esempio salvare l'indirizzo di ritorno), in modo da salvare il registro sulla pila può essere più efficiente che scrivere di nuovo a "i" -. Più di cache amichevole che l'accesso a un blocco completamente separata della memoria

Purtroppo, però, la funzione di blocco di rilascio non sa che la variabile non è stato scritto alla memoria ancora, quindi può fare nulla per risolvere il problema. Dopo tutto, che la funzione è solo una chiamata di libreria (il vero blocco-sblocco può essere nascosta in una chiamata più profondamente nidificato) e quella biblioteca potrebbe essere stato compilato anni prima che l'applicazione - non sa come suoi chiamanti utilizzano registri o la pila. Questa è una grande parte del motivo per cui usiamo uno stack, e le convenzioni di chiamata perché deve essere standardizzato (ad esempio, che salva i registri). La funzione di blocco rilascio non può costringere chiamanti di registri "sincronizzare".

Allo stesso modo, si potrebbe ricollegare una vecchia app con una nuova libreria - il chiamante non sa cosa "release_lock_on_i" fa o come, è solo una chiamata di funzione. lo fanon sa che ha bisogno di salvare i registri indietro per la memoria prima.

Per risolvere questo, siamo in grado di riportare la "volatile".

void myfunc ()
{
  volatile static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

Possiamo utilizzare una variabile locale normale temporaneamente mentre il blocco è attivo, per dare al compilatore la possibilità di utilizzare un registro per quel periodo breve. In linea di massima, però, un blocco dovrebbe essere rilasciato al più presto possibile, in modo da non ci dovrebbero essere più di tanto il codice in là. Se lo facciamo, però, scriviamo la nostra schiena variabile temporanea per "i" prima di rilasciare il blocco, e la volatilità dei "i" assicura che è scritto di nuovo alla memoria principale.

In linea di principio, questo non è sufficiente. Scrivendo alla memoria principale non vuol dire che hai scritto alla memoria principale - ci sono strati di cache per attraversare in mezzo, i dati potrebbe sedersi in uno qualsiasi di questi strati per un po '. V'è un problema di "memoria barriera" qui, e io non so molto su questo -. Ma per fortuna questo problema è la responsabilità di sincronizzazione dei thread chiama come i acquisire blocco e di sblocco chiamate superiori

Questo problema barriera di memoria non rimuove la necessità per la parola chiave "volatile", tuttavia.

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