68

スピンロックのダムバージョンを作成しようとしています。Web を閲覧していると、x86 で「PAUSE」と呼ばれるアセンブリ命令に出会いました。これは、この CPU でスピンロックが現在実行されていることをプロセッサに示すために使用されます。インテルのマニュアルおよび入手可能なその他の情報には、次のように記載されています

プロセッサはこのヒントを使用して、ほとんどの状況でメモリ順序違反を回避します。これにより、プロセッサのパフォーマンスが大幅に向上します。このため、すべてのスピン待機ループに PAUSE 命令を配置することをお勧めします。ドキュメントには、「待機(一部の遅延)」が命令の疑似実装であることも記載されています。

上記の段落の最後の行は直感的です。ロックの取得に失敗した場合は、再度ロックを取得する前にしばらく待つ必要があります。

しかし、スピンロックの場合のメモリ順序違反とは何を意味するのでしょうか? 「メモリ順序違反」は、スピンロック後の命令の誤った投機的ロード/ストアを意味しますか?

スピンロックの質問は以前にスタックオーバーフローで尋ねられましたが、メモリ順序違反の質問は未回答のままです (少なくとも私の理解では)。

4

2 に答える 2

104

プロセッサが典型的なスピン待機ループをどのように実行するか想像してみてください。

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:

数回の反復の後、分岐予測子は、条件付き分岐 (3) が発生せず、パイプラインが CMP 命令で満たされる (2) と予測します。これは、最終的に別のプロセッサが lockvar にゼロを書き込むまで続きます。この時点で、パイプラインは投機的な (つまり、まだコミットされていない) CMP 命令でいっぱいになり、そのうちのいくつかは既に lockvar を読み取り、次の条件分岐 (3) (これも投機的) に (誤った) ゼロ以外の結果を報告しました。これは、メモリ順序違反が発生したときです。プロセッサは、外部書き込み (別のプロセッサからの書き込み) を「検出」するたびに、同じメモリ位置に投機的にアクセスし、まだコミットしていない命令をパイプラインで検索します。そのような命令が見つかった場合、プロセッサの投機状態は無効になり、パイプライン フラッシュで消去されます。

残念ながら、このシナリオは (非常に可能性が高い) プロセッサがスピンロックを待機するたびに繰り返され、これらのロックが本来よりもはるかに遅くなります。

PAUSE 命令を入力します。

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    PAUSE            ; Wait for memory pipeline to become empty
5    JMP Spin_Lock
6 Get_Lock:

PAUSE 命令は、最初の例のように、パイプラインが投機的な CMP (2) 命令でいっぱいにならないように、メモリ読み取りを「パイプライン解除」します。(つまり、すべての古いメモリ命令がコミットされるまで、パイプラインをブロックする可能性があります。) CMP 命令 (2) は順次実行されるため、CMP 命令 (2) の読み取り後に外部書き込みが発生する可能性はほとんどありません (つまり、時間枠がはるかに短い)。 lockvar ですが、CMP がコミットされる前です。

もちろん、「デパイプライン化」はスピンロックでのエネルギーの浪費も少なく、ハイパースレッディングの場合は、他のスレッドがより適切に使用できるリソースを浪費しません。一方で、各ループが終了する前に発生するのを待っている分岐予測ミスがまだあります。Intelのドキュメントは、PAUSEがそのパイプラインのフラッシュを排除することを示唆していませんが、誰が知っています...

于 2012-10-15T21:56:16.123 に答える
6

@Mackie が言うように、パイプラインは s でいっぱいになりcmpます。Intel はcmp、別のコアが書き込みを行うときにこれらの をフラッシュする必要がありますが、これはコストのかかる操作です。CPU がフラッシュしない場合は、メモリ順序違反が発生しています。このような違反の例は次のとおりです。

(これは lock1 = lock2 = lock3 = var = 1 で始まります)

スレッド 1:

spin:
cmp lock1, 0
jne spin
cmp lock3, 0 # lock3 should be zero, Thread 2 already ran.
je end # Thus I take this path
mov var, 0 # And this is never run
end:

スレッド 2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.

まず、スレッド 1 について考えます。

cmp lock1, 0; jne spinbranch が lock1 がゼロでないと予測した場合、パイプラインに追加さcmp lock3, 0れます。

パイプラインでcmp lock3, 0lock3 を読み取り、それが 1 であることを確認します。

ここで、スレッド 1 がゆっくりと時間をかけており、スレッド 2 がすぐに実行を開始するとします。

lock3 = 0
lock1 = 0

それでは、スレッド 1 に戻りましょう。

cmp lock1, 0が最後に lock1 を読み取り、lock1 が 0 であることを発見し、その分岐予測能力に満足しているとしましょう。

このコマンドはコミットされ、何もフラッシュされません。正しい分岐予測は、プロセッサが内部依存関係がないと推測したため、順不同の読み取りがあっても何もフラッシュされないことを意味します。CPU から見ると lock3 は lock1 に依存していないため、これで問題ありません。

これで、cmp lock3, 0lock3 が 1 に等しいことを正しく読み取った がコミットします。

je end取られず、mov var, 0実行されます。

スレッド 3 では、ebxは 0 です。これは不可能だったはずです。これは、Intel が補正する必要があるメモリ順序違反です。


現在、インテルがその無効な動作を回避するために取っている解決策は、フラッシュすることです。スレッド 2 で実行するlock3 = 0と、スレッド 1 に lock3 を使用する命令を強制的にフラッシュさせます。この場合のフラッシュは、lock3 を使用するすべての命令がコミットされるまで、スレッド 1 がパイプラインに命令を追加しないことを意味します。スレッド 1cmp lock3がコミットできる前に、コミットするcmp lock1必要があります。がcmp lock1コミットを試みると、lock1 が実際には 1 に等しく、分岐予測が失敗したことが読み取られます。これにより、cmpが投げ出されます。スレッド 1 がフラッシュされたのでlock3、スレッド 1 のキャッシュ内の の場所が に設定され0、スレッド 1 は実行を継続します (待機中lock1)。スレッド 2 は、他のすべてのコアが使用をフラッシュしたことを通知されるようになりましたlock3キャッシュを更新したため、スレッド 2 は実行を継続します (その間、スレッド 2 は独立したステートメントを実行しますが、次の命令は別の書き込みであったため、他のコアに保留中のlock1 = 0書き込みを保持するためのキューがない限り、おそらくハングする必要があります)。

このプロセス全体はコストがかかるため、一時停止します。PAUSE はスレッド 1 を支援します。スレッド 1 は差し迫った分岐の予測ミスから即座に回復でき、正しく分岐する前にパイプラインをフラッシュする必要はありません。PAUSE は同様に、スレッド 1 のフラッシュを待つ必要がないスレッド 2 を支援します (前に述べたように、この実装の詳細についてはよくわかりませんが、スレッド 2 があまりにも多くの他のコアで使用されるロックを書き込もうとすると、スレッド 2 は最終的にはフラッシュを待つ必要があります)。

重要な理解として、私の例ではフラッシュが必要ですが、Mackie の例ではそうではありません。ただし、CPU には知る方法がないため (連続するステートメントの依存関係のチェックと分岐予測キャッシュを除いて、コードをまったく分析しません)、CPU はlockvar私の場合と同様に、Mackie の例にアクセスする命令をフラッシュします。正確性を保証するため。

于 2018-08-01T18:44:40.483 に答える