2013-04-11 9 views
8

Sto scrivendo un oggetto thread-safe che fondamentalmente rappresenta una doppia e utilizza una serratura per garantire una lettura e una scrittura sicure. Uso molti di questi oggetti (20-30) in un pezzo di codice che sta leggendo e scrivendoli tutti 100 volte al secondo e sto misurando il tempo di calcolo medio di ciascuna di queste fasi temporali. Ho iniziato a esaminare alcune opzioni per implementazioni del mio getter e dopo aver eseguito molti test e raccolto molti campioni per calcolare la mia misurazione del tempo di calcolo, ho riscontrato che alcune implementazioni sono sempre migliori di altre, ma non le implementazioni che mi aspetterei.C# differenze prestazionali thread-safe getter

Attuazione 1) Calcolo tempo medio = 0.607ms:

protected override double GetValue() 
{ 
    lock(_sync) 
    { 
     return _value; 
    } 
} 

attuazione 2) tempo di calcolo della media = 0.615ms:

protected override double GetValue() 
{ 
    double result; 
    lock(_sync) 
    { 
     result = _value; 
    } 
    return result; 
} 

Attuazione 3) tempo di calcolo media = 0.560ms:

protected override double GetValue() 
{ 
    double result = 0; 
    lock(_sync) 
    { 
     result = _value; 
    } 
    return result; 
} 

Cosa mi aspettavo: Mi aspettavo che l'implementazione 3 fosse la peggiore del 3 (questo era in realtà il mio codice originale, quindi era la possibilità o la codifica pigra che l'avevo scritto in questo modo), ma sorprendentemente è costantemente il migliore in termini di prestazioni. Mi aspetto che l'implementazione 1 sia la più veloce. Mi aspettavo anche che l'implementazione 2 fosse almeno altrettanto veloce, se non addirittura più veloce dell'implementazione 3, dal momento che sto semplicemente rimuovendo un assegnamento al doppio risultato che viene comunque sovrascritto, quindi non è necessario.

La mia domanda è: qualcuno può spiegare perché queste 3 implementazioni hanno le prestazioni relative che ho misurato? Mi sembra controintuitivo e mi piacerebbe davvero sapere perché.

Mi rendo conto che queste differenze non sono importanti, ma la loro misura relativa è coerente ogni volta che eseguo il test, raccogliendo migliaia di campioni ogni test per calcolare la media del tempo di calcolo. Inoltre, tieni presente che sto facendo questi test perché la mia applicazione richiede prestazioni molto elevate, o almeno quanto posso ragionevolmente ottenerla. Il mio test case è solo un piccolo test case e le prestazioni del mio codice saranno importanti durante il rilascio.

MODIFICA: si noti che sto utilizzando MonoTouch e sto eseguendo il codice su un dispositivo Mini iPad, quindi forse non è correlato a C# e altro ancora relativo al cross-compilatore di MonoTouch.

+1

Considerando quanto siano quasi identici (anche da un punto di vista dell'IL non mi aspetto che siano computazionalmente _che_ molto diverso) e quanto siano vicini i tempi di riferimento, sospetto che il grande colpevole sia il vostro metodo di prova. È compilato per la modalità 'release'? Come stai benchmark? Gli altri processi sul computer possono interferire con le risorse/tempo di elaborazione? Stai simulando la contesa della serratura, e se sì, come? EDIT: Inoltre, sospetto che la tua applicazione nel mondo reale abbia un lavoro più complicato nel blocco 'lock'? Perché così com'è, il lucchetto mi sembra superfluo. –

+3

1) Senza contesa per il blocco, i test non hanno senso. –

+0

per favore mostra il codice che stai usando per testare –

risposta

5

misurazione legge solo per la concorrenza è ingannevole, la cache vi darà ordini di grandezza risultati migliori rispetto caso sarebbe reale utilizzo. Così ho aggiunto ImpostaValore per l'esempio di Marc:

using System; 
using System.Diagnostics; 
using System.Threading; 

abstract class Experiment 
{ 
    public abstract double GetValue(); 
    public abstract void SetValue(double value); 
} 

class Example1 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     lock (_sync) 
     { 
      return _value; 
     } 
    } 

    public override void SetValue(double value) 
    { 
     lock (_sync) 
     { 
      _value = value; 
     } 

    } 

} 
class Example2 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     lock (_sync) 
     { 
      return _value; 
     } 
    } 

    public override void SetValue(double value) 
    { 
     lock (_sync) 
     { 
      _value = value; 
     } 
    } 

} 



class Example3 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     double result = 0; 
     lock (_sync) 
     { 
      result = _value; 
     } 
     return result; 
    } 

    public override void SetValue(double value) 
    { 
     lock (_sync) 
     { 
      _value = value; 
     } 
    } 
} 

class CompareExchange : Experiment 
{ 
    private double _value = 3; 
    public override double GetValue() 
    { 
     return Interlocked.CompareExchange(ref _value, 0, 0); 
    } 

    public override void SetValue(double value) 
    { 
     Interlocked.Exchange(ref _value, value); 
    } 
} 
class ReadUnsafe : Experiment 
{ 
    private long _value = DoubleToInt64(3); 
    static unsafe long DoubleToInt64(double val) 
    { // I'm mainly including this for the field initializer 
     // in real use this would be manually inlined 
     return *(long*)(&val); 
    } 
    public override unsafe double GetValue() 
    { 
     long val = Interlocked.Read(ref _value); 
     return *(double*)(&val); 
    } 

    public override void SetValue(double value) 
    { 
     long intValue = DoubleToInt64(value); 
     Interlocked.Exchange(ref _value, intValue); 
    } 
} 
class UntypedBox : Experiment 
{ 
    // references are always atomic 
    private volatile object _value = 3.0; 
    public override double GetValue() 
    { 
     return (double)_value; 
    } 

    public override void SetValue(double value) 
    { 
     object valueObject = value; 
     _value = valueObject; 
    } 
} 
class TypedBox : Experiment 
{ 
    private sealed class Box 
    { 
     public readonly double Value; 
     public Box(double value) { Value = value; } 

    } 
    // references are always atomic 
    private volatile Box _value = new Box(3); 
    public override double GetValue() 
    { 
     Box value = _value; 
     return value.Value; 
    } 

    public override void SetValue(double value) 
    { 
     Box boxValue = new Box(value); 
     _value = boxValue; 
    } 
} 
static class Program 
{ 
    static void Main() 
    { 
     // once for JIT 
     RunExperiments(1); 
     // three times for real 
     RunExperiments(5000000); 
     RunExperiments(5000000); 
     RunExperiments(5000000); 
    } 
    static void RunExperiments(int loop) 
    { 
     Console.WriteLine("x{0}", loop); 
     RunExperiment(new Example1(), loop); 
     RunExperiment(new Example2(), loop); 
     RunExperiment(new Example3(), loop); 
     RunExperiment(new CompareExchange(), loop); 
     RunExperiment(new ReadUnsafe(), loop); 
     RunExperiment(new UntypedBox(), loop); 
     RunExperiment(new TypedBox(), loop); 
     Console.WriteLine(); 
    } 
    static void RunExperiment(Experiment test, int loop) 
    { 
     // avoid any GC interruptions 
     GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 
     GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 
     GC.WaitForPendingFinalizers(); 

     int threads = Environment.ProcessorCount; 

     ManualResetEvent done = new ManualResetEvent(false); 

     // Since we use threads, divide the original workload 
     // 
     int workerLoop = Math.Max(1, loop/Environment.ProcessorCount); 
     int writeRatio = 1000; 
     int writes = Math.Max(workerLoop/writeRatio, 1); 
     int reads = workerLoop/writes; 

     var watch = Stopwatch.StartNew(); 

     for (int t = 0; t < Environment.ProcessorCount; ++t) 
     { 
      ThreadPool.QueueUserWorkItem((state) => 
       { 
        try 
        { 
         double val = 0; 

         // Two loops to avoid comparison for % in the inner loop 
         // 
         for (int j = 0; j < writes; ++j) 
         { 
          test.SetValue(j); 
          for (int i = 0; i < reads; i++) 
          { 
           val = test.GetValue(); 
          } 
         } 
        } 
        finally 
        { 
         if (0 == Interlocked.Decrement(ref threads)) 
         { 
          done.Set(); 
         } 
        } 
       }); 
     } 
     done.WaitOne(); 
     watch.Stop(); 
     Console.WriteLine("{0}\t{1}ms", test.GetType().Name, 
      watch.ElapsedMilliseconds); 

    } 
} 

I risultati sono, a 1000: 1 lettura:

x5000000 
Example1  353ms 
Example2  395ms 
Example3  369ms 
CompareExchange 150ms 
ReadUnsafe  161ms 
UntypedBox  11ms 
TypedBox  9ms 

100: 1 (leggi: scrittura)

x5000000 
Example1  356ms 
Example2  360ms 
Example3  356ms 
CompareExchange 161ms 
ReadUnsafe  172ms 
UntypedBox  14ms 
TypedBox  13ms 

10 rapporto di scrivere : 1 (leggi: scrivi)

x5000000 
Example1  383ms 
Example2  394ms 
Example3  414ms 
CompareExchange 169ms 
ReadUnsafe  176ms 
UntypedBox  41ms 
TypedBox  43ms 

2: 1 (leggi: scrivi)

x5000000 
Example1  550ms 
Example2  581ms 
Example3  560ms 
CompareExchange 257ms 
ReadUnsafe  292ms 
UntypedBox  101ms 
TypedBox  122ms 

1: 1 (leggi: scrittura)

x5000000 
Example1  718ms 
Example2  745ms 
Example3  730ms 
CompareExchange 381ms 
ReadUnsafe  376ms 
UntypedBox  161ms 
TypedBox  200ms 

* Aggiornato il codice per rimuovere le operazioni ICX inutili sulla scrittura, in quanto il valore viene sovrascritto sempre. Fissato anche la formula per calcolare il numero di letture da dividere per thread (stesso lavoro).

+0

Grazie per aver aggiunto anche SetValue, poiché penso che faccia la differenza. Domanda: non è costoso creare nuove istanze di Box nell'implementazione di TypedBox? È per questo che il suo tempo di esecuzione inizia a insinuarsi mentre aumenti la percentuale di scritture nei risultati? – Camputer

+0

Per rispondere alla tua domanda si dovrebbe effettivamente [profilo] (http://msdn.microsoft.com/en-us/library/z9z62c29.aspx) il codice. –

+0

Follow-up molto approfondito, grazie. Per informazioni, intendevo aggiungere "volatile" all'esempio box/typed-box - altrimenti potrei leggere sporco - il mio errore. –

15

Francamente, ci sono altri approcci migliori qui. Le seguenti uscite (ignorando la x1, che è per JIT):

x5000000 
Example1  128ms 
Example2  136ms 
Example3  129ms 
CompareExchange 53ms 
ReadUnsafe  54ms 
UntypedBox  23ms 
TypedBox  12ms 

x5000000 
Example1  129ms 
Example2  129ms 
Example3  129ms 
CompareExchange 52ms 
ReadUnsafe  53ms 
UntypedBox  23ms 
TypedBox  12ms 

x5000000 
Example1  129ms 
Example2  161ms 
Example3  129ms 
CompareExchange 52ms 
ReadUnsafe  53ms 
UntypedBox  23ms 
TypedBox  12ms 

Tutte queste sono filo implementazioni sicure. Come puoi vedere, la più veloce è una casella dattiloscritta, seguita da una casella non codificata (object). Il prossimo arriva (all'incirca alla stessa velocità) Interlocked.CompareExchange/Interlocked.Read - nota che quest'ultimo supporta solo long, quindi dobbiamo fare un po 'di bit-bashing per trattarlo come uno double.

Ovviamente, test sul framework di destinazione.

Per divertimento, ho anche testato un Mutex; sullo stesso test di scala, che richiede circa 3300 ms.

using System; 
using System.Diagnostics; 
using System.Threading; 
abstract class Experiment 
{ 
    public abstract double GetValue(); 
} 
class Example1 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     lock (_sync) 
     { 
      return _value; 
     } 
    } 
} 
class Example2 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     lock (_sync) 
     { 
      return _value; 
     } 
    } 
} 

class Example3 : Experiment 
{ 
    private readonly object _sync = new object(); 
    private double _value = 3; 
    public override double GetValue() 
    { 
     double result = 0; 
     lock (_sync) 
     { 
      result = _value; 
     } 
     return result; 
    } 
} 

class CompareExchange : Experiment 
{ 
    private double _value = 3; 
    public override double GetValue() 
    { 
     return Interlocked.CompareExchange(ref _value, 0, 0); 
    } 
} 
class ReadUnsafe : Experiment 
{ 
    private long _value = DoubleToInt64(3); 
    static unsafe long DoubleToInt64(double val) 
    { // I'm mainly including this for the field initializer 
     // in real use this would be manually inlined 
     return *(long*)(&val); 
    } 
    public override unsafe double GetValue() 
    { 
     long val = Interlocked.Read(ref _value); 
     return *(double*)(&val); 
    } 
} 
class UntypedBox : Experiment 
{ 
    // references are always atomic 
    private volatile object _value = 3.0; 
    public override double GetValue() 
    { 
     return (double)_value; 
    } 
} 
class TypedBox : Experiment 
{ 
    private sealed class Box 
    { 
     public readonly double Value; 
     public Box(double value) { Value = value; } 

    } 
    // references are always atomic 
    private volatile Box _value = new Box(3); 
    public override double GetValue() 
    { 
     return _value.Value; 
    } 
} 
static class Program 
{ 
    static void Main() 
    { 
     // once for JIT 
     RunExperiments(1); 
     // three times for real 
     RunExperiments(5000000); 
     RunExperiments(5000000); 
     RunExperiments(5000000); 
    } 
    static void RunExperiments(int loop) 
    { 
     Console.WriteLine("x{0}", loop); 
     RunExperiment(new Example1(), loop); 
     RunExperiment(new Example2(), loop); 
     RunExperiment(new Example3(), loop); 
     RunExperiment(new CompareExchange(), loop); 
     RunExperiment(new ReadUnsafe(), loop); 
     RunExperiment(new UntypedBox(), loop); 
     RunExperiment(new TypedBox(), loop); 
     Console.WriteLine(); 
    } 
    static void RunExperiment(Experiment test, int loop) 
    { 
     // avoid any GC interruptions 
     GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 
     GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 
     GC.WaitForPendingFinalizers(); 

     double val = 0; 
     var watch = Stopwatch.StartNew(); 
     for (int i = 0; i < loop; i++) 
      val = test.GetValue(); 
     watch.Stop(); 
     if (val != 3.0) Console.WriteLine("FAIL!"); 
     Console.WriteLine("{0}\t{1}ms", test.GetType().Name, 
      watch.ElapsedMilliseconds); 

    } 

} 
+0

Puoi rimuovere il modificatore pericoloso in TypedBox.GetValue? – Guillaume86

+0

@ Guillaume86 scusa, errore di copia-incolla; sì - cambierà; non cambia i numeri –

+0

@MarcGravell Grazie per i confronti molto interessanti! Ho una domanda, l'implementazione di TypedBox consente solo letture a causa della parola chiave readonly? Ho bisogno di leggere e scrivere in modo sicuro, semplicemente non ho incluso il codice di scrittura poiché non stavo testando questo nel mio post originale, anche se ho usato lo stesso meccanismo di blocco nel mio metodo SetValue. Se si aggiunge un metodo SetValue negli esempi di box tipizzati e non tipizzati, queste implementazioni sarebbero ancora thread-safe? Non vedo cosa rende la lettura in queste classi esclusiva/atomica, mi manca qualcosa? (nuovo in C#!) – Camputer