私はgccインラインアセンブリを使用するのは初めてで、x86マルチコアマシンでスピンロック(競合状態なし)を(AT&T構文を使用して)実装できるかどうか疑問に思っていました。
spin_lock: mov 0 eax ロックcmpxchg1[lock_addr] jnz spin_lock ret spin_unlock: lock mov 0 [lock_addr] ret
私はgccインラインアセンブリを使用するのは初めてで、x86マルチコアマシンでスピンロック(競合状態なし)を(AT&T構文を使用して)実装できるかどうか疑問に思っていました。
spin_lock: mov 0 eax ロックcmpxchg1[lock_addr] jnz spin_lock ret spin_unlock: lock mov 0 [lock_addr] ret
あなたは正しい考えを持っていますが、あなたのasmは壊れています:
cmpxchg
イミディエートオペランドでは機能せず、レジスタのみで機能します。
lock
の有効なプレフィックスではありませんmov
。 mov
整列されたアドレスへの変換はx86ではアトミックであるため、とにかく必要ありませlock
ん。
AT&T構文を使用してからしばらく経ちましたが、すべてを覚えていることを願っています。
spin_lock:
xorl %ecx, %ecx
incl %ecx # newVal = 1
spin_lock_retry:
xorl %eax, %eax # expected = 0
lock; cmpxchgl %ecx, (lock_addr)
jnz spin_lock_retry
ret
spin_unlock:
movl $0, (lock_addr) # atomic release-store
ret
GCCにはアトミックビルトインがあるため、これを実現するために実際にインラインasmを使用する必要はありません。
void spin_lock(int *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1));
}
void spin_unlock(int volatile *p)
{
asm volatile ("":::"memory"); // acts as a memory barrier.
*p = 0;
}
Boが以下に示すように、ロックされた命令にはコストがかかります。使用するすべての命令は、キャッシュラインへの排他的アクセスを取得し、実行中にロックダウンlock cmpxchg
する必要があります。たとえば、そのキャッシュラインへの通常のストアのように、lock cmpxchg
実行中は保持されます。これにより、特に複数のスレッドがロックの取得を待機している場合に、ロック解除スレッドが遅延する可能性があります。多くのCPUがなくても、最適化するのは簡単で価値があります。
void spin_lock(int volatile *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1))
{
// spin read-only until a cmpxchg might succeed
while(*p) _mm_pause(); // or maybe do{}while(*p) to pause first
}
}
このpause
命令は、このように回転するコードがある場合にハイパースレッディングCPUのパフォーマンスに不可欠です。これにより、最初のスレッドが回転している間に2番目のスレッドが実行されます。をサポートしていないCPUではpause
、として扱われますnop
。
pause
また、スピンループを離れるとき、最終的に実際の作業を再開するときのメモリ順序の誤推測を防ぎます。 x86の「PAUSE」命令の目的は何ですか?
スピンロックが実際に使用されることはめったにないことに注意してください。通常、クリティカルセクションやfutexなどを使用します。これらは、低競合下でのパフォーマンスのためにスピンロックを統合しますが、その後、OS支援のスリープおよび通知メカニズムにフォールバックします。彼らはまた、公平性を改善するための措置を講じる可能性があり、cmpxchg
/pause
ループが行わない他の多くのことを行います。
またcmpxchg
、単純なスピンロックには不要であることに注意してください。使用xchg
して、古い値が0であったかどうかを確認できます。ed命令内で行う作業が少なくなるlock
と、キャッシュラインが固定される時間が短くなる可能性があります。およびを使用した完全なasm実装については、インラインアセンブリを介したメモリ操作のロックを参照してください(ただし、OS支援スリープへのフォールバックはなく、無期限に回転するだけです)。xchg
pause
これにより、メモリバスでの競合が少なくなります。
void spin_lock(int *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1)) while(*p);
}
構文が間違っています。少し変更すると動作します。
spin_lock:
movl $0, %eax
movl $1, %ecx
lock cmpxchg %ecx, (lock_addr)
jnz spin_lock
ret
spin_unlock:
movl $0, (lock_addr)
ret
より高速に実行されるコードを提供するため。lock_addr
レディスターに保管されていると仮定し%rdi
ます。
スピンする代わりにとを使用movl
します。test
lock cmpxchgl %ecx, (%rdi)
lock cmpxchgl %ecx, (%rdi)
チャンスがある場合にのみクリティカルセクションに入ろうとするために使用します。
そうすれば、不要なバスのロックを回避できます。
spin_lock:
movl $1, %ecx
loop:
movl (%rdi), %eax
test %eax, %eax
jnz loop
lock cmpxchgl %ecx, (%rdi)
jnz loop
ret
spin_unlock:
movl $0, (%rdi)
ret
pthreadとこのような簡単なループを使用してテストしました。
for(i = 0; i < 10000000; ++i){
spin_lock(&mutex);
++count;
spin_unlock(&mutex);
}
私のテストでは、最初のテストは2.5〜3秒かかり、2番目のテストは1.3〜1.8秒かかります。