2012-01-25 11 views
17

Sono in esecuzione su una macchina a 32 bit e sono in grado di confermare che i valori lunghi possono lacerare utilizzando il seguente frammento di codice che colpisce molto velocemente.Simulare la rottura di una doppia in C#

 static void TestTearingLong() 
     { 
      System.Threading.Thread A = new System.Threading.Thread(ThreadA); 
      A.Start(); 

      System.Threading.Thread B = new System.Threading.Thread(ThreadB); 
      B.Start(); 
     } 

     static ulong s_x; 

     static void ThreadA() 
     { 
      int i = 0; 
      while (true) 
      { 
       s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL; 
       i++; 
      } 
     } 

     static void ThreadB() 
     { 
      while (true) 
      { 
       ulong x = s_x; 
       Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL); 
      } 
     } 

Ma quando provo qualcosa di simile con il doppio, non riesco a strappare. Qualcuno sa perché? Per quanto posso dire dalle specifiche, solo l'assegnazione a un float è atomica. L'assegnazione a un doppio dovrebbe avere il rischio di strappi.

static double s_x; 

    static void TestTearingDouble() 
    { 
     System.Threading.Thread A = new System.Threading.Thread(ThreadA); 
     A.Start(); 

     System.Threading.Thread B = new System.Threading.Thread(ThreadB); 
     B.Start(); 
    } 

    static void ThreadA() 
    { 
     long i = 0; 

     while (true) 
     { 
      s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue; 
      i++; 

      if (i % 10000000 == 0) 
      { 
       Console.Out.WriteLine("i = " + i); 
      } 
     } 
    } 

    static void ThreadB() 
    { 
     while (true) 
     { 
      double x = s_x; 

      System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue); 
     } 
    } 
+4

Domanda stupida - che cosa è lo strappo? – Oded

+0

le operazioni su ints sono garantite per essere atomiche per quanto riguarda l'accesso da più thread. Non così con i lunghi. Tearing sta ottenendo un mix di due valori intermedi (cattivo). Si sta chiedendo perché lo stesso non si vede in doppio, poiché anche i doppi non garantiscono le operazioni atomiche. – hatchet

+13

@Oded: su macchine a 32 bit, vengono scritti solo 32 bit alla volta. Se si sta scrivendo un valore a 64 bit su una macchina a 32 bit e si scrive allo stesso indirizzo contemporaneamente su due thread diversi, si hanno effettivamente * quattro * scritture, non * due *, perché le scritture vengono eseguite a 32 bit in un tempo. È quindi possibile che i thread si muovano e quando il fumo cancella la variabile contiene i primi 32 bit scritti da un thread e i 32 bit inferiori scritti dall'altro. Quindi puoi scrivere 0xDEADBEEF00000000 su un thread e 0x00000000BAADF00D su un altro e finire con 0x0000000000000000 in memoria. –

risposta

10
static double s_x; 

E 'molto più difficile da dimostrare l'effetto quando si utilizza un doppio. La CPU utilizza istruzioni dedicate per caricare e memorizzare un doppio, rispettivamente FLD e FSTP. È molto più semplice con lungo poiché non esiste una singola istruzione che carichi/memorizzi un numero intero a 64 bit in modalità a 32 bit. Per osservarlo è necessario avere l'indirizzo della variabile disallineato in modo tale che si trovi a cavallo del confine della cache della cpu.

Questo non avverrà mai con la dichiarazione che hai usato, il compilatore JIT assicura che il doppio sia allineato correttamente, memorizzato in un indirizzo che è un multiplo di 8. Puoi memorizzarlo in un campo di una classe, solo l'allocatore GC allinea a 4 nella modalità a 32 bit. Ma questa è una merda.

Il modo migliore per farlo è intenzionalmente disallineare il doppio utilizzando un puntatore.Mettere pericoloso davanti alla classe di programma e farlo sembrare simile a questo:

static double* s_x; 

    static void Main(string[] args) { 
     var mem = Marshal.AllocCoTaskMem(100); 
     s_x = (double*)((long)(mem) + 28); 
     TestTearingDouble(); 
    } 
ThreadA: 
      *s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue; 
ThreadB: 
      double x = *s_x; 

Questo ancora non garantisce una buona disallineamento (hehe), in quanto non c'è modo di controllare esattamente dove AllocCoTaskMem() allineerà l'allocazione relativa all'inizio della linea della cache della CPU. E dipende dall'associatività della cache nel core della CPU (il mio è un Core i5). Dovrai armeggiare con l'offset, ho ottenuto il valore 28 dalla sperimentazione. Il valore dovrebbe essere divisibile per 4, ma non per 8 per simulare veramente il comportamento dell'heap GC. Continua ad aggiungere 8 al valore finché non ottieni il doppio per cavalcare la linea della cache e attivare l'assert.

Per renderlo meno artificiale, devi scrivere un programma che memorizza il doppio campo di una classe e ottenere il garbage collector per spostarlo in memoria in modo che non sia allineato. È difficile trovare un programma di esempio che lo assicuri che questo accada.

Nota anche come il tuo programma può dimostrare un problema chiamato condivisione false. Commentare la chiamata al metodo Start() per il thread B e notare quanto è più veloce il thread A. Stai vedendo il costo della CPU mantenendo la linea della cache coerente tra i core della CPU. La condivisione è intesa qui poiché i thread accedono alla stessa variabile. La vera condivisione falsa avviene quando i thread accedono a variabili diverse che sono memorizzate nella stessa riga della cache. Questo è altrimenti il ​​motivo per cui l'allineamento è importante, puoi solo osservare lo strappo di un doppio quando parte di esso è in una linea della cache e parte di essa è in un'altra.

+0

Non capisco come l'attraversamento del confine della linea cache possa causare lo strappo. Pensavo che ciò fosse dovuto solo al fatto che il valore occupava più spazio della dimensione di un registro. Puoi per favore approfondire questo aspetto? – Tudor

+0

@Tudor: è un effetto completamente diverso, non associato alle dimensioni del registro. Concentrati sull'ultimo paragrafo, nota come la sincronizzazione della cache della cpu ha una linea della cache come unità di aggiornamento. Un doppio disallineamento che si trova a cavallo di una linea richiede * due * aggiornamenti, simili al modo in cui un lungo richiede due scritture di registro. Il che richiede abbastanza tempo per consentire il codice che viene eseguito su un altro core per osservare lo strappo. –

11

Per quanto strano possa sembrare, dipende dalla CPU. Mentre i doppi sono non sono garantiti non strappare, non lo faranno su molti processori attuali. Prova un Sempron AMD se vuoi strappare in questa situazione.

MODIFICA: l'ho imparato fino a pochi anni fa.

+0

È questo a che fare con le dimensioni dei registri in virgola mobile? – leppie

+0

TBH Non ho la minima idea, mai guardato dentro. Un mio demone (Free Pascal di tutte le lingue) ha iniziato a produrre spuriemente risultati assurdi su una e una sola macchina tra tante (forse 100), tutte impostate dalla stessa immagine ecc. Si è scoperto che era un doppio globale che è stato aggiornato da il thread principale e un thread secondario creato da GTK. Nessun primitivo di blocco in FPK quindi ... (imprecazione, imprecazione) –

+0

Sì, non dubiterei se le estensioni MMX o SSE della CPU avessero qualcosa a che fare con questo. – antiduh

0

Facendo qualche scavo, ho trovato alcuni interessanti legge concernente operazioni a virgola mobile sulle architetture x86:

numeri a virgola mobile

Secondo Wikipedia, x86 unità di calcolo in virgola mobile memorizzati nei registri 80 bit:

[...] successivi processori x86 poi integrato questa funzionalità x87 on chip che ha reso l'istruzioni x87 de facto parte integrante del set di istruzioni x86. Ogni registro x87, noto come ST (0) tramite ST (7), ha una larghezza di 80 bit e memorizza i numeri nel formato di precisione doppio esteso a virgola mobile IEEE .

Anche questa domanda SO altro è legato: Some floating point precision and numeric limits question

Questo potrebbe spiegare perché, anche se sono doppie a 64-bit, sono operati atomicamente.

0

Per quale valore questo argomento e codice di esempio è disponibile qui.

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx

+0

Questo articolo parla solo di lungo, non di doppio. – Tudor

+0

concordato. In realtà, penso che il codice di esempio che ho postato nella domanda provenga da quel post (eccetto per il doppio). (L'ho avuto in un progetto di test e me ne sono dimenticato per un po '). –