web-dev-qa-db-ja.com

ロック解除されたミューテックスのロックはどれくらい効率的ですか?ミューテックスのコストはいくらですか?

低レベル言語(C、C++など):多数のミューテックス(pthreadが提供するものやネイティブシステムライブラリが提供するものなど)を使用するか、オブジェクト用に1つを使用するかを選択できます。

ミューテックスをロックするのはどれくらい効率的ですか?つまりアセンブラー命令がいくつある可能性があり、どれくらい時間がかかりますか(ミューテックスがロック解除されている場合)?

ミューテックスの費用はいくらですか?本当に多くのミューテックスを持つことは問題ですか?または、コードにint変数があるのと同じくらいのmutex変数をスローできますか?

(異なるハードウェア間でどれだけの違いがあるかわかりません。もしあれば、それらについても知りたいです。しかし、ほとんどの場合、一般的なハードウェアに興味があります。)

ポイントは、オブジェクト全体の単一のミューテックスではなく、それぞれがオブジェクトの一部のみをカバーする多数のミューテックスを使用することで、多くのブロックを安全にできることです。そして、私はこれについてどこまで行かなければならないのか疑問に思っています。つまりこれがどれほど複雑でミューテックスが多くても、可能な限りブロックを可能な限り安全に保護する必要がありますか?


ロックに関するWebKitsブログ投稿(2016) は、この質問に非常に関連しており、スピンロック、適応ロック、フューテックスなどの違いを説明しています。

131
Albert

多数のミューテックスを使用するか、オブジェクトに対して1つのミューテックスを使用するかを選択できます。

多数のスレッドがあり、オブジェクトへのアクセスが頻繁に発生する場合、複数のロックにより並列性が向上します。ロックを増やすとロックのデバッグが増えるため、保守性が犠牲になります。

ミューテックスをロックするのはどれくらい効率的ですか?つまりアセンブラー命令はどれくらいある可能性があり、どれくらい時間がかかりますか(ミューテックスがロック解除されている場合)?

正確なアセンブラ命令は、 mutex - メモリ/キャッシュの一貫性 のオーバーヘッドが最小であり、保証が主なオーバーヘッドです。また、特定のロックが取得される頻度は少なくなります。

ミューテックスは、2つの主要な部分(単純化)で構成されています:(1)ミューテックスがロックされているかどうかを示すフラグ、および(2)待機キュー。

フラグの変更はほんの数命令で、通常はシステムコールなしで行われます。 mutexがロックされている場合、syscallは呼び出しスレッドを待機キューに追加し、待機を開始します。待機キューが空の場合のロック解除は安価ですが、それ以外の場合は待機プロセスの1つを起動するためにsyscallが必要です。 (一部のシステムでは、ミューテックスを実装するために安価/高速のシステムコールが使用され、競合の場合にのみ低速(通常)システムコールになります。)

ロックされていないミューテックスをロックするのは本当に安いです。競合のないmutexのロック解除も安価です。

ミューテックスの費用はいくらですか?本当に多くのミューテックスを持つのは問題ですか?または、int変数があるのと同じくらい多くのmutex変数をコードにスローできますか?

必要なだけミューテックス変数をコードにスローできます。アプリケーションが割り当てることができるメモリの量によってのみ制限されます。

概要。ユーザー空間のロック(特にミューテックス)は安価であり、システムの制限を受けません。しかし、それらのあまりに多くはデバッグのために悪夢を綴ります。単純なテーブル:

  1. ロックが少ないということは、より多くの競合(遅いシステムコール、CPUストール)とより少ない並列性を意味します
  2. ロックが少ないと、マルチスレッドの問題をデバッグする際の問題が少なくなります。
  3. ロックが多いほど、競合が少なくなり、並列性が高くなります
  4. ロックが多いほど、デバッグできないデッドロックに陥る可能性が高くなります。

アプリケーションのバランスの取れたロックスキームを見つけて維持する必要があります。通常は、2番目と3番目のバランスを取ります。


(*)あまり頻繁にロックされないミューテックスの問題は、アプリケーションのロックが多すぎると、CPU間/コアトラフィックの多くが他のCPUのデータキャッシュからミューテックスメモリをフラッシュして、キャッシュの一貫性。キャッシュフラッシュは軽量の割り込みに似ており、CPUによって透過的に処理されますが、いわゆる stalls (「ストール」の検索)を導入します。

また、ストールが原因で、ロックコードの実行速度が遅くなります。多くの場合、アプリケーションが遅い理由は明らかではありません。 (一部のArchはCPU /コア間のトラフィック統計を提供しますが、一部は提供しません。)

この問題を回避するために、人々は通常、ロックの競合の可能性を減らし、ストールを回避するために多数のロックに頼ります。これが、システムの制限を受けない安価なユーザースペースロックが存在する理由です。

100
Dummy00001

同じことを知りたかったので、測定しました。私のボックス(AMD FX(tm)-8150 Eight-Core Processor at 3.612361 GHz)では、独自のキャッシュラインにあり、既にキャッシュされているロック解除されたミューテックスをロックおよびロック解除するには47クロック(13 ns)かかります。

2つのコア間の同期のため(CPU#0と#1を使用しました)、2つのスレッドで102 nsごとにロック/ロック解除ペアを呼び出すことができたため、51 nsごとに1回、約38時間がかかると結論付けることができますnsは、次のスレッドが再びロックできるようになる前に、スレッドがロック解除を行った後に回復します。

これを調査するために使用したプログラムは、次の場所にあります: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

私のボックスに特有のハードコードされた値(xrange、yrange、rdtscオーバーヘッド)がいくつかあることに注意してください。

その状態で生成されるグラフは次のとおりです。

enter image description here

これは、次のコードでベンチマークを実行した結果を示しています。

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

2つのrdtsc呼び出しは、「mutex」をロックおよびロック解除するのにかかるクロック数を測定します(私のボックスのrdtsc呼び出しのオーバーヘッドは39クロックです)。 3番目のasmは遅延ループです。遅延ループのサイズは、スレッド0の場合よりもスレッド1の場合の方が1カウント小さいため、スレッド1はわずかに高速です。

上記の関数は、サイズ100,000のタイトループで呼び出されます。関数はスレッド1の方がわずかに高速ですが、mutexの呼び出しにより両方のループが同期します。これは、ロック/ロック解除ペアで測定されたクロック数がスレッド1でわずかに大きいため、その下のループの遅延が短くなっていることを示すグラフからわかります。

上記のグラフでは、右下のポイントは、loop_countの遅延が150の測定値です。そして、左下のポイントに続いて、loop_countが測定ごとに1つずつ減少します。 77になると、関数は両方のスレッドで102 nsごとに呼び出されます。その後loop_countをさらに減らすと、スレッドを同期できなくなり、ミューテックスがほとんどの時間で実際にロックされ始め、ロック/ロック解除にかかるクロック量が増加します。また、これにより、関数呼び出しの平均時間が増加します。そのため、プロットポイントは再び右に向かって上昇します。

このことから、50 nsごとにミューテックスをロックおよびロック解除することは、私のボックスでは問題ではないと結論付けることができます。

結局のところ、OPの質問に対する答えは、競合が少なくなる限り、ミューテックスを追加する方が良いということです。

ミューテックスはできるだけ短くロックしてください。それらをループの外側に置く唯一の理由は、そのループが100 ns(または50 nsの同じ時間にそのループを実行したいスレッドの数)ごとに1回以上ループする場合、または13 ns回の場合ですループサイズは、競合による遅延よりも遅延が大きくなります。

編集:私は今、この主題についてより多くの知識を得て、ここで提示した結論を疑い始めました。まず、CPU 0と1はハイパースレッド化されています。 AMDが8個の実際のコアを持っていると主張していても、他の2つのコア間の遅延がはるかに大きい(つまり、0と1がペアを形成するため、2と3、4と5、6と7が非常に厄介なものです) )。第二に、std :: mutexは、mutexのロックをすぐに取得できない場合にシステムコールを実際に実行する前に少しロックをスピンするように実装されます(間違いなく非常に遅くなります)。したがって、ここで測定したのは絶対的に最も理想的な状況であり、実際にはロックおよびロック解除はロック/ロック解除ごとに大幅に時間がかかる可能性があります。

結論として、ミューテックスはアトミックで実装されます。コア間でアトミックを同期するには、内部バスをロックして、対応するキャッシュラインを数百クロックサイクルの間フリーズさせる必要があります。ロックを取得できない場合、スレッドをスリープ状態にするためにシステムコールを実行する必要があります。それは明らかに非常に遅いです。そのスレッドはとにかくスリープする必要があるため、通常は実際には問題ではありませんが、スレッドが通常スピンしている間にロックを取得できないため、システムコールを行うことができる高い競合の問題になる可能性がありますが、CANその後すぐにロックを取得します。たとえば、複数のスレッドが密なループでミューテックスをロックおよびロック解除し、それぞれが1マイクロ秒程度ロックを保持する場合、それらのスレッドは絶えずスリープ状態になり、再び起動されるため、非常に遅くなる可能性があります。

17
Carlo Wood

これは、実際に「ミューテックス」と呼ぶもの、OSモードなどに依存します。

最小では、インターロックメモリ操作のコストです。 (他のプリミティブアセンブラコマンドと比較して)比較的重い操作です。

ただし、それは非常に高くなる可能性があります。 「mutex」と呼ばれるものがカーネルオブジェクト(つまり、OSによって管理されるオブジェクト)で、ユーザーモードで実行される場合、その操作はすべてカーネルモードトランザクションにつながります。これはvery heavyです。

たとえば、Intel Core Duoプロセッサ、Windows XP。連動操作:約40 CPUサイクルかかります。カーネルモードコール(システムコール)-約2000 CPUサイクル。

この場合、クリティカルセクションの使用を検討してください。これは、カーネルミューテックスとインターロックメモリアクセスのハイブリッドです。

10
valdo

コストは実装によって異なりますが、次の2つの点に留意する必要があります。

  • それはかなり原始的な操作であり、その使用パターンのために可能な限り最適化されるため、コストはおそらく最小です(lot)。
  • 安全なマルチスレッド操作が必要な場合は使用する必要があるため、どれほど高価でもかまいません。必要な場合は、必要です。

通常、シングルプロセッサシステムでは、データをアトミックに変更するのに十分な時間だけ割り込みを無効にすることができます。マルチプロセッサシステムでは、 test-and-set 戦略を使用できます。

どちらの場合も、命令は比較的効率的です。

大規模なデータ構造に単一のミューテックスを提供するべきか、それとも各セクションに1つのミューテックスを多数持つべきかということは、バランスをとる行為です。

単一のミューテックスを使用すると、複数のスレッド間で競合が発生するリスクが高くなります。セクションごとにミューテックスを持つことでこのリスクを減らすことができますが、仕事をするためにスレッドが180個のミューテックスをロックしなければならない状況にはなりたくないです:-)

6
paxdiablo

私はpthreadとmutexを初めて使用しますが、実験から、競合がない場合はmutexのロック/ロック解除のコストはほぼゼロであることを確認できますが、競合がある場合はブロッキングのコストが非常に高くなります。私は、タスクが相互排他ロックで保護されたグローバル変数の合計を計算するだけのスレッドプールで簡単なコードを実行しました。

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

1つのスレッドで、プログラムは10,000,000の値をほぼ瞬時に(1秒未満で)合計します。 2スレッド(4コアのMacBook)では、同じプログラムに39秒かかります。

1
Grant Petty