Measuring only reads for concurrency is misleading, your cache will give you orders of magnitude better results than real use case would. So I added SetValue to Marc's example:
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);
}
}
Results are, at 1000:1 read:write ratio:
x5000000
Example1 353ms
Example2 395ms
Example3 369ms
CompareExchange 150ms
ReadUnsafe 161ms
UntypedBox 11ms
TypedBox 9ms
100:1 (read:write)
x5000000
Example1 356ms
Example2 360ms
Example3 356ms
CompareExchange 161ms
ReadUnsafe 172ms
UntypedBox 14ms
TypedBox 13ms
10:1 (read:write)
x5000000
Example1 383ms
Example2 394ms
Example3 414ms
CompareExchange 169ms
ReadUnsafe 176ms
UntypedBox 41ms
TypedBox 43ms
2:1 (read:write)
x5000000
Example1 550ms
Example2 581ms
Example3 560ms
CompareExchange 257ms
ReadUnsafe 292ms
UntypedBox 101ms
TypedBox 122ms
1:1 (read:write)
x5000000
Example1 718ms
Example2 745ms
Example3 730ms
CompareExchange 381ms
ReadUnsafe 376ms
UntypedBox 161ms
TypedBox 200ms
*Updated the code to remove the unnecessary ICX operations on write, since the value is overwritten always. Also fixed the formula to compute the number of reads to divide by threads (same work).