2013-04-11 13 views
8

Piszę obiekt bezpieczny wątku, który w zasadzie reprezentuje podwójne i używa blokady, aby zapewnić bezpieczne czytanie i pisanie. Używam wielu z tych obiektów (20-30) w kawałku kodu, który czyta i zapisuje je wszystkie 100 razy na sekundę, i mierzę średni czas obliczania każdego z tych kroków czasowych. Zacząłem przyglądać się kilku opcjom implementacji mojego gettera i po przeprowadzeniu wielu testów i zebraniu wielu próbek w celu uśrednienia mojego pomiaru czasu obliczeń uważam, że pewne implementacje działają konsekwentnie lepiej niż inne, ale nie implementacje, których oczekiwałbym.C# wątek bezpieczne różnice wydajności getter

Wykonanie 1) Obliczenie średni czas = 0.607ms:

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

wykonawcze 2) czas obliczeń średniej = 0.615ms:

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

Realizacja 3) czas obliczeń średniej = 0.560ms:

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

Czego się spodziewałem: Spodziewałem się, że implementacja 3 będzie najgorsza z 3 (to był właściwie mój oryginalny kod, więc było to przypadkowe lub leniwe kodowanie, które napisałem w ten sposób), ale zaskakująco jest to konsekwentnie najlepsze pod względem wydajności. Spodziewałbym się, że wdrożenie 1 będzie najszybsze. Spodziewałem się również, że implementacja 2 będzie co najmniej tak szybka, jeśli nie szybsza niż implementacja 3, ponieważ właśnie usuwam przypisanie do podwójnego wyniku, który i tak jest nadpisywany, więc nie jest to konieczne.

Moje pytanie brzmi: Czy ktoś może wyjaśnić, dlaczego te 3 wdrożenia mają względną wydajność, którą zmierzyłem? Wydaje mi się to sprzeczne z intuicją i naprawdę chciałbym wiedzieć dlaczego.

Zdaję sobie sprawę, że różnice te nie są duże, ale ich względna miara jest stała za każdym razem, gdy wykonuję test, zbierając tysiące próbek w każdym teście, aby uśrednić czas obliczeń. Pamiętaj też, że wykonuję te testy, ponieważ moja aplikacja wymaga bardzo wysokiej wydajności lub przynajmniej tak dobrej jak to tylko możliwe. Mój przypadek testowy to tylko mały przypadek testowy, a wydajność mojego kodu będzie ważna przy uruchomieniu w wersji.

EDYCJA: Zwróć uwagę, że używam MonoTouch i uruchamiam kod na urządzeniu iPad Mini, więc być może nie ma to nic wspólnego z C# i czymś więcej związanym z kompilatorem krzyża MonoTouch.

+1

Biorąc pod uwagę ich prawie identyczne (nawet z perspektywy IL nie oczekuję, że będą obliczeniowo - to znacznie różni) i jak bliskie są czasy testów, podejrzewam, że wielkim winowajcą jest twoja metoda testowania. Czy jest skompilowany dla trybu "release"? Jak się masz benchmarking? Czy inne procesy na komputerze mogą zakłócać przetwarzanie zasobów/czasu? Czy w ogóle symulujesz rywalizację o zamek, a jeśli tak, to w jaki sposób? EDYCJA: Ponadto, podejrzewam, że Twoja aplikacja w świecie rzeczywistym ma bardziej skomplikowaną pracę w bloku 'blokady'? Bo jak jest, zamek wydaje mi się zbędny. –

+3

1) Bez rywalizacji o zamek testy są bez znaczenia. –

+0

pokaż kod, którego używasz do przetestowania –

Odpowiedz

5

Pomiar czyta tylko dla współbieżności jest mylące, pamięć podręczną daje rzędy wielkości lepsze wyniki niż stosowanie byłoby rzeczywistym przypadku. Więc dodałem setValue przykładu Marca:

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); 

    } 
} 

Wyniki na 1000: 1 Czytaj: napisać stosunek:

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

100: 1 (czytaj: zapis)

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

10 : 1 (odczyt: zapis)

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

2: 1 (czytaj: zapis)

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

1: 1 (czytaj: write)

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

* Aktualizacja kodu, aby usunąć niepotrzebne operacje ICX przy zapisie, ponieważ wartość jest zawsze nadpisane. Poprawiono również formułę, aby obliczyć liczbę odczytów do podziału przez wątki (ta sama praca).

+0

Dziękujemy za dodanie wartości SetValue również tam, ponieważ myślę, że to robi różnicę. Pytanie: czy tworzenie instancji Boxa w implementacji TypedBox nie jest drogie? Czy właśnie dlatego jego czas realizacji zaczyna się podkradać, gdy zwiększasz proporcje zapisów w swoich wynikach? – Camputer

+0

Aby odpowiedzieć na twoje pytanie, należałoby faktycznie [profil] (http://msdn.microsoft.com/en-us/library/z9z62c29.aspx) kod. –

+0

Bardzo dokładne śledzenie, dzięki. Dla informacji, miałem na celu dodanie "volatile" w polu/typowym pudełku - w przeciwnym razie mógłby się zabrudzić czyta - mój błąd. –

15

Szczerze mówiąc, istnieją inne, lepsze podejścia tutaj. Następujące wyjścia (ignorując x1, który jest dla 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 

Wszystkie te implementacje są wątkami bezpiecznymi. Jak widać, najszybszym z nich jest wpisane pole, a następnie pole bez typu (object). Następnie przychodzi (przy mniej więcej tej samej prędkości) Interlocked.CompareExchange/Interlocked.Read - zauważ, że ta ostatnia obsługuje tylko long, więc musimy zrobić trochę bit-bashowania, aby traktować to jako double.

Oczywiście przetestuj docelowy framework.

Dla zabawy przetestowałem także Mutex; w tej samej skali, która trwa około 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

Czy możesz usunąć niebezpieczny modyfikator w TypedBox.GetValue? – Guillaume86

+0

@ Guillaume86 przepraszam, skopiuj i wklej błąd; tak - zmieni; nie zmienia numerów –

+0

@MarcGravell Dziękujemy za bardzo interesujące porównania! Mam pytanie, czy implementacja TypedBox pozwala tylko na czytanie ze względu na słowo kluczowe readonly? Muszę bezpiecznie czytać i pisać, po prostu nie uwzględniłem kodu pisania, ponieważ nie testowałem tego w moim oryginalnym wpisie, chociaż użyłem tego samego mechanizmu blokującego w mojej metodzie SetValue. Jeśli dodasz metodę SetValue w przykładowych i niepoprawionych przykładach pudełek, czy te implementacje nadal będą bezpieczne dla wątków? Nie rozumiem, co sprawia, że ​​odczyt w tych klasach jest wyjątkowy/atomowy, czy czegoś brakuje? (nowy na C#!) – Camputer