マークが投稿したもののより効率的 (バスのロックと読み取りが少ない) で単純化された実装:
static int InterlockedIncrementAndClamp(ref int count, int max)
{
int oldval = Volatile.Read(ref count), val = ~oldval;
while(oldval != max && oldval != val)
{
val = oldval;
oldval = Interlocked.CompareExchange(ref count, oldval + 1, oldval);
}
return oldval + 1;
}
競合が非常に多い場合は、一般的なケースを単一のアトミック インクリメント命令に減らすことで、スケーラビリティをさらに改善できる可能性があります。CompareExchange と同じオーバーヘッドですが、ループの可能性はありません。
static int InterlockedIncrementAndClamp(ref int count, int max, int drift)
{
int v = Interlocked.Increment(ref count);
while(v > (max + drift))
{
// try to adjust value.
v = Interlocked.CompareExchange(ref count, max, v);
}
return Math.Min(v, max);
}
ここでは、を超える値count
まで上げることができます。しかし、まだ までしか戻りません。これにより、ほとんどの場合、オペレーション全体を 1 つのアトミック インクリメントに折りたたむことができ、最大のスケーラビリティが可能になります。値を超えた場合にのみ複数の操作が必要になります。これは、非常にまれにするのに十分な大きさにすることができます。drift
max
max
drift
インターロックメモリアクセスと非インターロックメモリアクセスが連携して動作することについての Marc の懸念に応えて:
具体的にはvolatile
vs Interlocked:volatile
は通常のメモリ操作ですが、最適化されていないものであり、他のメモリ操作に関して並べ替えられていないものです。この特定の問題は、これらの特定のプロパティのいずれにも関係していないため、実際には、非インターロック対インターロックの相互運用性について話している.
.NET メモリ モデルは、基本的な整数型 (マシンのネイティブ ワード サイズまで) の読み取りと書き込みを保証し、参照はアトミックです。Interlocked メソッドもアトミックです。.NET には「アトミック」の定義が 1 つしかないため、相互に互換性があることを明示的に特殊なケースにする必要はありません。
Volatile.Read
保証されていないことの 1 つは、可視性です。常にロード命令を取得しますが、CPU は、別の CPU によってメモリに置かれたばかりの新しい値ではなく、ローカル キャッシュから古い値を読み取る可能性があります。MOVNTPS
x86 では、ほとんどの場合 (例外のような特別な命令)、これについて心配する必要はありませんが、他のアーキテクチャでは非常に可能性があります。
要約すると、これは に影響を与える可能性のある 2 つの問題を説明していVolatile.Read
ます。まず、16 ビット CPU で実行しているint
可能性があります。第二に、アトミックであっても、可視性のために古い値を読み取っている可能性があります。
しかし、影響Volatile.Read
を与えるということは、それらがアルゴリズム全体に影響を与えるという意味ではなく、これらから完全に保護されています。
最初のケースは、非アトミックな方法で同時に書き込みを行っている場合にのみ、私たちを悩ませます。count
これは、(A[0] を書き込む; CAS A[0:1] を書き込む; A[1] を書き込む) という結果になる可能性があるためです。書き込みはすべてcount
保証されたアトミック CAS で行われるため、これは問題ではありません。読んでいるときに間違った値を読み取ると、次の CAS でキャッチされます。
考えてみると、2 番目のケースは実際には、読み取りと書き込みの間で値が変化する通常のケースの特殊化にすぎません。読み取りは、要求する前に行われます。この場合、最初のInterlocked.CompareExchange
呼び出しは与えられたものとは異なる値を報告し、Volatile.Read
成功するまでループを開始します。
Volatile.Read
必要に応じて、競合が少ない場合の純粋な最適化と考えることができます。で初期化できoldval
、0
それでも問題なく動作します。を使用Volatile.Read
すると、2 つではなく 1 つの CAS のみを実行する可能性が高くなります (これは、命令が進むにつれて、特にマルチ CPU 構成では非常にコストがかかります)。
しかし、そうです、Marc が言うように、時にはロックの方がシンプルな場合もあります!