web-dev-qa-db-ja.com

x86の「PAUSE」命令の目的は何ですか?

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

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

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

しかし、スピンロックの場合のメモリ順序違反とはどういう意味ですか? 「メモリ順序違反」とは、スピンロック後の命令の不正な投機的ロード/ストアを意味しますか?

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

53

想像してみてください、プロセッサーが典型的なスピンウェイトループをどのように実行するか:

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がコミットされる前。

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

75
Mackie Messer

@Mackieが言うように、パイプラインはcmpsで満たされます。 Intelは、別のコアが書き込むときにこれらのcmpsをフラッシュする必要がありますが、これは負荷の高い操作です。 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 spinブランチは、lock1がゼロではないと予測した場合、cmp lock3, 0をパイプラインに追加します。

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

ここで、スレッド1が適切な時間を費やしており、スレッド2がすぐに実行を開始するとします。

lock3 = 0
lock1 = 0

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

cmp lock1, 0が最後にlock1を読み取り、lock1が0であることがわかり、その分岐予測機能に満足しているとします。

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

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

je endは使用されず、mov var, 0が実行されます。

スレッド3では、ebxは0です。これは不可能でした。これはIntelが補償しなければならないメモリ順序違反です。


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

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

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

3