16

私はgccインラインアセンブリを使用するのは初めてで、x86マルチコアマシンでスピンロック(競合状態なし)を(AT&T構文を使用して)実装できるかどうか疑問に思っていました。

spin_lock:
mov 0 eax
ロックcmpxchg1[lock_addr]
jnz spin_lock
ret

spin_unlock:
lock mov 0 [lock_addr]
ret
4

3 に答える 3

26

あなたは正しい考えを持っていますが、あなたのasmは壊れています:

cmpxchgイミディエートオペランドでは機能せず、レジスタのみで機能します。

lockの有効なプレフィックスではありませんmovmov整列されたアドレスへの変換は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支援スリープへのフォールバックはなく、無期限に回転するだけです)。xchgpause

于 2011-08-04T02:36:52.230 に答える
2

これにより、メモリバスでの競合が少なくなります。

void spin_lock(int *p)
{
    while(!__sync_bool_compare_and_swap(p, 0, 1)) while(*p);
}
于 2012-10-16T21:22:22.547 に答える
0

構文が間違っています。少し変更すると動作します。

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します。testlock 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秒かかります。

于 2019-12-23T12:41:02.227 に答える