同時実行の読み取りのみを測定することは誤解を招きます。キャッシュは、実際の使用例よりも桁違いに優れた結果をもたらします。そこで、Marc の例に SetValue を追加しました。
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);
}
}
結果は、1000:1 の読み取り:書き込み比率で次のようになります。
x5000000
Example1 353ms
Example2 395ms
Example3 369ms
CompareExchange 150ms
ReadUnsafe 161ms
UntypedBox 11ms
TypedBox 9ms
100:1 (読み取り:書き込み)
x5000000
Example1 356ms
Example2 360ms
Example3 356ms
CompareExchange 161ms
ReadUnsafe 172ms
UntypedBox 14ms
TypedBox 13ms
10:1 (読み取り:書き込み)
x5000000
Example1 383ms
Example2 394ms
Example3 414ms
CompareExchange 169ms
ReadUnsafe 176ms
UntypedBox 41ms
TypedBox 43ms
2:1 (読み取り:書き込み)
x5000000
Example1 550ms
Example2 581ms
Example3 560ms
CompareExchange 257ms
ReadUnsafe 292ms
UntypedBox 101ms
TypedBox 122ms
1:1 (読み取り:書き込み)
x5000000
Example1 718ms
Example2 745ms
Example3 730ms
CompareExchange 381ms
ReadUnsafe 376ms
UntypedBox 161ms
TypedBox 200ms
*値は常に上書きされるため、書き込み時に不要な ICX 操作を削除するようにコードを更新しました。また、読み取り数をスレッドで割る計算式を修正しました (同じ作業)。