2015-06-20 7 views
32

stavo navigando l'albero dei sorgenti di .NET core oggi e attraversò di corsa this pattern in System.Collections.Immutable.ImmutableArray<T>:Che vantaggio c'è di memorizzare "questo" in una variabile locale in un metodo struct?

T IList<T>.this[int index] 
{ 
    get 
    { 
     var self = this; 
     self.ThrowInvalidOperationIfNotInitialized(); 
     return self[index]; 
    } 
    set { throw new NotSupportedException(); } 
} 

Questo modello (memorizzazione this in una variabile locale) sembra essere coerentemente applicato in questo file ogni volta che altrimenti sarebbe fatto riferimento this più volte nello stesso metodo, ma non quando viene fatto riferimento una sola volta. Così ho iniziato a pensare a quali potrebbero essere i relativi vantaggi nel farlo in questo modo; mi sembra che il vantaggio sia probabilmente legato alle prestazioni, quindi ho seguito questa strada un po 'oltre ... forse trascurerò qualcos'altro.

Il CIL che viene emesso per il "negozio this in un locale" modello sembra essere simile a un ldarg.0, poi ldobj UnderlyingType, quindi stloc.0 in modo che i riferimenti successivi provengono da ldloc.0 invece di una nuda ldarg.0 come sarebbe ad appena utilizzare più volte this.

Forse ldarg.0 è significativamente più lento di ldloc.0, ma non abbastanza sia per il C# to-CIL traduzione o il jitter a cercare nuove opportunità per ottimizzare questo per noi, in modo tale che ha più senso di scrivere questo strano dall'aspetto modello in codice C# in qualsiasi momento altrimenti emetteremo due istruzioni ldarg.0 in un metodo di istanza struct?

Update: o, sai, ho potuto guardato i commenti all'inizio di quel file, che explain esattamente quello che sta succedendo ...

risposta

20

Come già notato, System.Collections.Immutable .ImmutableArray < T> è una struct :

public partial struct ImmutableArray<T> : ... 
{ 
    ... 

    T IList<T>.this[int index] 
    { 
     get 
     { 
      var self = this; 
      self.ThrowInvalidOperationIfNotInitialized(); 
      return self[index]; 
     } 
     set { throw new NotSupportedException(); } 
    } 

    ... 

var self = this; crea una copia della struct a cui si riferisce questo. Perché dovrebbe aver bisogno di farlo? Il source comments of this struct fornisce una spiegazione del motivo per cui è necessario:

/// Questo tipo deve essere thread-safe. Come una struttura, non può proteggere i suoi propri campi
/// venga modificata da un thread mentre i suoi membri sono in esecuzione su altri thread
/// perché le strutture possono cambiare in luogo semplicemente riassegnando campo contenente
/// questa struttura. Pertanto è estremamente importante che
/// ** Ogni membro dovrebbe solo dereferenziare questa volta. **
/// Se un membro deve fare riferimento al campo dell'array, ciò vale come dereferenziamento di questo.
/// Anche chiamare altri membri di istanza (proprietà o metodi) conta come dereferenziare questo.
/// Ogni membro che deve utilizzarlo più di una volta deve invece
/// assegnarlo a una variabile locale e utilizzarlo per il resto del codice.
/// Copia efficacemente un campo nella struttura in una variabile locale in modo che
/// sia isolato da altri thread.

In breve, se è possibile che altri thread stanno facendo modifiche a un campo della struttura o cambiare la struct in posizione (riassegnando un campo membro della classe di questo tipo struct, per esempio), mentre il metodo get è in esecuzione e quindi potrebbe causare effetti collaterali negativi, quindi diventa necessario che il metodo get faccia prima una copia (locale) della struct prima di elaborarla.

Aggiornamento: Si prega di leggere anche supercats answer, che spiega in dettaglio quali condizioni devono essere soddisfatte in modo che un'operazione come fare una copia locale di una struttura (cioè var self = this;) è di essere thread-safe, e che cosa potrebbe accadere se tali condizioni sono non soddisfatti.

+0

Hmm, ma considerando che 'System.Collections.Immutable.ImmutableArray ' è immutabile, se questa è l'unica considerazione, allora questo modello non è semplicemente un modo per masterizzare alcuni cicli della CPU in più rispetto al contrario? –

+0

Ora sono curioso. se guardate questo thread http://stackoverflow.com/questions/9648939/c-sharp-structs-questo è quindi possibile impostare 'this = ...' se chiamando 'this' crea una nuova istanza del struct? – Benj

+0

@ Benj, cosa intendi con "chiamare questo"? Nota che una struct è un tipo di valore, quindi un assegnamento "=" copia la struct ... – elgonzo

8

Le istanze di struttura in .NET sono sempre modificabili se la posizione di archiviazione sottostante è modificabile e sempre immutabile se il percorso di archiviazione sottostante è immutabile. È possibile che i tipi di strutture "fingano" di essere immutabili, ma .NET consentirà alle istanze di tipo struttura di essere modificate da qualsiasi elemento che possa scrivere le posizioni di archiviazione in cui risiedono, e i tipi di struttura non hanno voce in capitolo.

Pertanto, se uno aveva una struct:

struct foo { 
    String x; 
    override String ToString() { 
    String result = x; 
    System.Threading.Thread.Sleep(2000); 
    return result & "+" & x; 
    } 
    foo(String xx) { x = xx; } 
} 

e si dovesse richiamare il seguente metodo su due fili con la stessa matrice myFoos di tipo foo[]:

myFoos[0] = new foo(DateTime.Now.ToString()); 
var st = myFoos[0].ToString(); 

sarebbe del tutto possibile che qualsiasi thread avviato per primo avrebbe il suo valore ToString() riportare il tempo scritto dalla sua chiamata al costruttore e il tempo riportato dalla chiamata del costruttore dell'altro thread, piuttosto che riportare la stessa stringa due volte. Per i metodi il cui scopo è quello di convalidare un campo di struttura e quindi utilizzarlo, se il campo cambia tra la convalida e l'uso, il metodo verrà utilizzato utilizzando un campo non convalidato. Copiando il contenuto del campo della struttura (copiando solo il campo o copiando l'intera struttura) si evita tale pericolo.

noti che per le strutture che contengono un campo di tipo Int64, UInt64 o Double, o che contengono più di un campo, è possibile che una dichiarazione come var temp=this; che si verifica in un thread mentre un altro thread sovrascrive la posizione in cui this è stato archiviato, potrebbe finire per copiare una struttura che contiene una miscela arbitraria di vecchi e nuovi contenuti. Solo quando una struttura contiene un singolo campo di un tipo di riferimento, o un singolo campo di una primitiva a 32 bit o più piccola, è garantito che una lettura che si verifica simultaneamente con una scrittura produrrà un certo valore che la struttura effettivamente possiede, e anche questo potrebbe avere qualche stranezza (es. almeno in VB.NET, una dichiarazione come someField = New foo("george") potrebbe cancellare someField prima di chiamare il costruttore).

+0

* Le istanze di struttura in .NET sono sempre modificabili se la posizione di archiviazione sottostante è modificabile e sempre immutabile se la posizione di archiviazione sottostante è immutabile. * A volte le cose dovrebbero essere scritte in caratteri di fuoco nel cielo, in modo che chiunque possa leggerle. Oggi è la prima volta che scopro che una 'struct' immutabile potrebbe essere non-thread safe ... Se penso che sia abbastanza chiaro ...' int64' può essere soggetto a strappi (le parti alta e bassa non sono modificate -at-the-same-time) se più thread leggono e scrivono allo stesso tempo ... quindi è chiaro che qualsiasi 'struct' potrebbe essere soggetto ad esso. – xanatos

+0

@xanatos: una struttura che contiene un singolo campo di riferimento o un campo singolo di tipo "breve" primitivo, sarà immune allo strappo.In molti casi in cui è necessario racchiudere un singolo riferimento a un'istanza immutabile di un tipo mutabile, una struttura può offrire prestazioni migliori e una semantica migliore di una classe [ad es. se 'BigInteger' fosse una struct con un singolo campo di tipo' int [] '(usando i primi elementi dell'array per contenere cose come il valore hash memorizzato nella cache, ecc.) allora sarebbe possibile per' default (BigInteger) ' comportarsi come valore zero, piuttosto che come riferimento null.] – supercat

+0

Sì ... lo strappo è una classe di problemi che si intersecano a quello di cui stiamo parlando qui ... Comunque è strano che, alla fine, tu non posso nemmeno essere sicuro che 'int.GetHashcode()' sia sicuro o meno thread (è, perché è semplicemente 'return this') ma' short.GetHashCode' probabilmente non lo è, perché è: 'return (int) ((ushort) this) | (int) questo << 16; ' – xanatos

Problemi correlati