これまでは、プロジェクトで_std::queue
_を使用していました。このキューの特定の操作に必要な平均時間を測定しました。
時間は2台のマシンで測定しました:ローカルのUbuntu VMとリモートサーバー。_std::queue
_を使用すると、平均は両方のマシンでほぼ同じでした:750マイクロ秒。
次に、_std::queue
_を_boost::lockfree::spsc_queue
_に「アップグレード」したため、キューを保護しているミューテックスを取り除くことができました。ローカルでVM大きなパフォーマンスの向上が見られました。平均は200マイクロ秒になりました。しかし、リモートマシンでは平均が800マイクロ秒になり、以前よりも遅くなりました。 。
まず、リモートマシンがロックフリー実装をサポートしていない可能性があるためだと考えました。
すべてのハードウェアが同じアトミック命令のセットをサポートするわけではありません。ハードウェアで使用できない場合は、ガードを使用してソフトウェアでエミュレートできます。ただし、これにはロックフリープロパティが失われるという明らかな欠点があります。
これらの命令がサポートされているかどうかを確認するために、_boost::lockfree::queue
_にはbool is_lock_free(void) const;
というメソッドがあります。ただし、_boost::lockfree::spsc_queue
_にはこのような関数はありません。これは、私にとっては、ハードウェアに依存せず、どのマシンでも常にロックフリーであることを意味します。
パフォーマンスが低下する理由は何ですか?
_// c++11 compiler and boost library required
#include <iostream>
#include <cstdlib>
#include <chrono>
#include <async>
#include <thread>
/* Using blocking queue:
* #include <mutex>
* #include <queue>
*/
#include <boost/lockfree/spsc_queue.hpp>
boost::lockfree::spsc_queue<int, boost::lockfree::capacity<1024>> queue;
/* Using blocking queue:
* std::queue<int> queue;
* std::mutex mutex;
*/
int main()
{
auto producer = std::async(std::launch::async, [queue /*,mutex*/]()
{
// Producing data in a random interval
while(true)
{
/* Using the blocking queue, the mutex must be locked here.
* mutex.lock();
*/
// Push random int (0-9999)
queue.Push(std::Rand() % 10000);
/* Using the blocking queue, the mutex must be unlocked here.
* mutex.unlock();
*/
// Sleep for random duration (0-999 microseconds)
std::this_thread::sleep_for(std::chrono::microseconds(Rand() % 1000));
}
}
auto consumer = std::async(std::launch::async, [queue /*,mutex*/]()
{
// Example operation on the queue.
// Checks if 1234 was generated by the producer, returns if found.
while(true)
{
/* Using the blocking queue, the mutex must be locked here.
* mutex.lock();
*/
int value;
while(queue.pop(value)
{
if(value == 1234)
return;
}
/* Using the blocking queue, the mutex must be unlocked here.
* mutex.unlock();
*/
// Sleep for 100 microseconds
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
consumer.get();
std::cout << "1234 was generated!" << std::endl;
return 0;
}
_
一般に、ロックフリーアルゴリズムは、ロックベースのアルゴリズムよりもパフォーマンスが低下します。それは、彼らがそれほど頻繁に使用されない主な理由です。
ロックフリーアルゴリズムの問題は、競合するスレッドが競合を継続できるようにすることで競合を最大化することです。ロックは、競合するスレッドのスケジュールを解除することにより、競合を回避します。ロックフリーアルゴリズムは、最初の近似値として、競合するスレッドのスケジュールを解除できない場合にのみ使用してください。アプリケーションレベルのコードにはほとんど適用されません。
非常に極端な仮説をお見せしましょう。典型的な最新のデュアルコアCPUで4つのスレッドが実行されていると想像してください。スレッドA1とA2はコレクションAを操作しています。スレッドB1とB2はコレクションBを操作しています。
まず、コレクションがロックを使用することを想像してみましょう。つまり、スレッドA1とA2(またはB1とB2)が同時に実行しようとすると、スレッドの1つがロックによってブロックされます。そのため、非常に迅速に、1つのAスレッドと1つのBスレッドが実行されます。これらのスレッドは非常に高速に実行され、競合しません。スレッドが競合しようとすると、競合するスレッドのスケジュールが解除されます。わーい。
ここで、コレクションがロックを使用しないことを想像してください。現在、スレッドA1とA2は同時に実行できます。これにより、一定の競合が発生します。コレクションのキャッシュラインは、2つのコア間でピンポンします。コア間バスが飽和状態になる場合があります。パフォーマンスはひどくなります。
繰り返しますが、これは非常に誇張されています。しかし、あなたはアイデアを得る。競合を避けたい、できるだけ多くのことで苦しむ必要はありません。
ただし、システム全体でA1とA2が唯一のスレッドである場合、この思考実験を再度実行してください。現在、ロックのないコレクションの方がおそらく優れています(ただし、その場合はスレッドを1つだけ持つ方が良いかもしれませんが!)。
ほとんどすべてのプログラマーは、ロックが悪いと考え、ロックを回避することでコードが高速になると考える段階を経ます。最終的に、彼らはそれが競合であることを認識します。
可能性のあるすべてのケースで、ブーストロックフリーキューが遅いとは言えません。私の経験では、プッシュ(定数T&アイテム)はコピーを作成しようとしています。 tmpオブジェクトを作成してキューをプッシュしている場合、パフォーマンスドラッグの影響を受けます。ライブラリでは、移動可能なオブジェクトをより効率的にするために、オーバーロードされたバージョンのPush(T && item)が必要だと思います。新しい関数を追加する前に、ポインター、プレーンタイプ、またはC++ 11以降に提供されているスマートなポインターを使用する必要がある場合があります。これはキューのかなり限定された側面であり、ロックフリーキューを使用することはめったにありません。