二重チェックのロックは必要ありません。ほとんどの場合、ロックを回避するためにプラットフォームに依存するメモリオーダリングのトリックに依存する純粋なパフォーマンスの最適化です。シングルチェックロックは常に十分で、移植性が高く、単純であるため、ベンチマークでその小さなブーストが本当に必要であることが示されない限り、ロックを優先する必要があります。しかし、さらに良い解決策があります。
明示的にロックされたシングルトンは完全に避ける必要があります。代わりに、静的初期化子を試してください。静的初期化子は単純で、(スレッド)セーフで、高速で、遅延ロードされるためです。
sealed class MyHeavyClass
{
MyHeavyClass() {}
public static readonly MyHeavyClass Instance = new MyHeavyClass();
}
このようなインスタンスは、アプリケーションの起動時に作成されませんが、最初にタイプまたはフィールドが最初に使用される前に、やや遅れて作成されます。正確なルールは、静的コンストラクターが存在するかどうかによって異なりますが、初期化子が必要以上に早く実行されることがある場合は、これで問題ありません。.NET v4では、初期化は非常に怠惰です。99%の確率で、これは可能な限り最速で最も単純な実装であるため、頼りになる実装になるはずです。.NET v3.5以前でも、タイプを参照するメソッドが検出されるまで、これはロードされません。
クラスが完全にロードされると、フィールドにアクセスする際にガードやロックが不要になるため、このコードはロックベースのバージョンよりも高速になる可能性があります。特に、JITは変数が設定されていると単純に想定でき、理論的にはnullチェックやループからのホイスト読み取りなどを省略できます。遅延読み込みのタイミングを正確に制御する必要がある場合。ダブルチェックされたロック(いくつかのトリッキーなメモリモデルの詳細に依存します)ではなく、より単純なロックしてからチェックしてみてください。しかし、実際には、その正確な制御を必要とする人はほとんどいないと思います。不必要な作業を避けたいだけです。
ダブルチェックロックに関して:私が知る限り、.NETでも、volatile
ダブルチェックロックのキーワードを完全に移植可能にする必要があります。.NETのARM実装には、あなたと同じ書き込み順序の保証はありません。 x86で使用されました。ARMやさまざまなモノラルプラットフォームで動作する場合でも、単純な静的初期化子よりも遅いのに、なぜこのような複雑な実装を使用するのでしょうか。
ベンチマーク結果
AlwaysLock init
反復あたり37.09ナノ秒(AlwaysLockの1000000イター)
DoubleCheckedLocking init
反復あたり2.78ナノ秒(DoubleCheckedLockingの1000000反復)
StaticInitializer init
反復あたり2.13ナノ秒(StaticInitializerの1000000イター)
StaticConstructor init
反復あたり2.56ナノ秒(StaticConstructorの1000000イター)
反復あたり38.45ナノ秒(AlwaysLockの10000000イター)
反復あたり2.07ナノ秒(DoubleCheckedLockingの10000000反復)
反復あたり1.57ナノ秒(StaticInitializerの10000000イター)
反復あたり1.57ナノ秒(StaticConstructorの10000000反復)
反復あたり21.71ナノ秒(AlwaysLockの10000000同期反復)
反復あたり4.62ナノ秒(DoubleCheckedLockingの10000000同期反復)
反復あたり3.15ナノ秒(StaticInitializerの10000000同期反復子)
反復あたり3.17ナノ秒(StaticConstructorの10000000同期反復)
ベンチマークコード
void Main()
{
const int loopSize = 10000000;
Bench(loopSize/10, ()=> AlwaysLock.Inst);
Bench(loopSize/10, ()=> DoubleCheckedLocking.Inst);
Bench(loopSize/10, ()=> StaticInitializer.Inst);
Bench(loopSize/10, ()=> StaticConstructor.Inst);
Console.WriteLine();
Bench(loopSize, ()=> AlwaysLock.Inst);
Bench(loopSize, ()=> DoubleCheckedLocking.Inst);
Bench(loopSize, ()=> StaticInitializer.Inst);
Bench(loopSize, ()=> StaticConstructor.Inst);
Console.WriteLine();
SBench(loopSize, ()=> AlwaysLock.Inst);
SBench(loopSize, ()=> DoubleCheckedLocking.Inst);
SBench(loopSize, ()=> StaticInitializer.Inst);
SBench(loopSize, ()=> StaticConstructor.Inst);
//uncommenting the next lines will cause instantiation of
//StaticInitializer but not StaticConstructor right before this method.
//var o = new object[]{
// StaticInitializer.Inst, StaticConstructor.Inst};
}
static void Bench<T>(int iter, Func<T> func) {
string name = func().GetType().Name;
var sw = Stopwatch.StartNew();
Parallel.For(0,iter,i=>func());
var sec = sw.Elapsed.TotalSeconds;
Console.Write("{0:f2} nanoseconds per iteration ({1} iters of {2})\n"
, sec*1000*1000*1000/iter, iter, name);
}
static void SBench<T>(int iter, Func<T> func) {
string name = func().GetType().Name;
var sw = Stopwatch.StartNew();
for(int i=0;i<iter;i++) func();
var sec = sw.Elapsed.TotalSeconds;
Console.Write("{0:f2} nanoseconds per iteration ({1} sync iters of {2})\n"
, sec*1000*1000*1000/iter, iter, name);
}
sealed class StaticInitializer {
StaticInitializer(){ Console.WriteLine("StaticInitializer init"); }
public static readonly StaticInitializer Inst = new StaticInitializer();
//no static constructor, initialization happens before
//the method with the first access
}
sealed class StaticConstructor {
StaticConstructor(){ Console.WriteLine("StaticConstructor init"); }
//a static constructor prevents initialization before the first access.
static StaticConstructor(){}
public static readonly StaticConstructor Inst = new StaticConstructor();
}
sealed class AlwaysLock {
AlwaysLock(){ Console.WriteLine("AlwaysLock init"); }
static readonly object _lock = new object();
static AlwaysLock _instance;
public static AlwaysLock Inst { get {
lock(_lock)
if (_instance == null)
_instance = new AlwaysLock();
return _instance;
} }
}
sealed class DoubleCheckedLocking {
DoubleCheckedLocking(){ Console.WriteLine("DoubleCheckedLocking init"); }
static readonly object _lock = new object();
static DoubleCheckedLocking _instance;
public static DoubleCheckedLocking Inst { get {
if (_instance == null)
lock(_lock)
if (_instance == null)
_instance = new DoubleCheckedLocking();
return _instance;
} }
}
TL; DR
シングルトンにロックを使用せず、静的初期化子を使用します。