C ++でのスレッド間の高速メッセージパッシングのためのメモリ管理


9

2つのスレッドがあり、互いに非同期でデータメッセージを非同期に送信することによって通信するとします。各スレッドには、ある種のメッセージキューがあります。

私の質問は非常に低いレベルです。メモリを管理する最も効率的な方法として何が期待できますか?私はいくつかの解決策を考えることができます:

  1. 送信者はを介してオブジェクトを作成しますnew。受信者の呼び出しdelete
  2. メモリプーリング(メモリを送信者に転送するため)
  3. ガベージコレクション(Boehm GCなど)
  4. (オブジェクトが十分に小さい場合)ヒープ割り当てを完全に回避するために値でコピー

1)は最も明白なソリューションなので、プロトタイプに使用します。おそらくそれはすでに十分であるということです。しかし、私の特定の問題とは関係なく、パフォーマンスを最適化する場合、どの手法が最も有望かと思います。

特にスレッド間の情報の流れに関する追加の知識を使用できるので、プールは理論的には最高だと思います。でも、それが一番難しいのも怖いです。たくさんのチューニング... :-(

ガベージコレクションは後で(ソリューション1の後)非常に簡単に追加できるはずであり、非常にうまく機能すると期待しています。したがって、1)が非効率的であることが判明した場合、それが最も実用的な解決策であると思います。

オブジェクトが小さくてシンプルな場合は、値によるコピーが最も高速な場合があります。ただし、サポートされるメッセージの実装に不必要な制限を強いることを恐れているので、避けたいと思います。

回答:


9

オブジェクトが小さくてシンプルな場合は、値によるコピーが最も高速な場合があります。ただし、サポートされるメッセージの実装に不必要な制限を強いることを恐れているので、避けたいと思います。

上限を予測できる場合char buf[256]、たとえば、まれな場合にヒープ割り当てのみを呼び出すことができない場合の実用的な代替策:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

3

キューの実装方法によって異なります。

配列(ラウンドロビンスタイル)を使用する場合は、ソリューション4のサイズに上限を設定する必要があります。リンクキューを使用する場合は、オブジェクトを割り当てる必要があります。

次に、リソースプーリングはあなただけの新しいを交換して削除したときに簡単に行うことができるAllocMessage<T>freeMessage<T>。私の提案は、Tコンクリートを割り当てるときに、潜在的なサイズの量を制限し、切り上げることmessagesです。

まっすぐなガベージコレクションは機能しますが、大きな部分を収集する必要がある場合に長い一時停止を引き起こす可能性があり、(私が思うに)新規/削除よりも少しパフォーマンスが低下します。


3

C ++の場合は、スマートポインターの1つを使用します。unique_ptrは、誰かがハンドルを取得するまで基になるオブジェクトを削除しないため、問題なく機能します。値によってptrオブジェクトをレシーバーに渡し、どのスレッドがそれを削除すべきかについて心配する必要はありません(レシーバーがオブジェクトを受信しない場合)。

スレッド間のロックを処理する必要はありますが、メモリがコピーされないため、パフォーマンスは良好です(小さなptrオブジェクト自体のみ)。

ヒープへのメモリの割り当てはこれまでで最速ではないため、プーリングを使用してこれを大幅に高速化します。プール内の事前にサイズ設定されたヒープから次のブロックを取得するだけなので、既存のライブラリを使用してください。


2
ロックは通常、メモリのコピーよりもはるかに大きな問題です。ただ言って。
tdammers 2012

あなたが書くときunique_ptr、私はあなたが意味してshared_ptrいると思います。しかし、スマートポインターの使用がリソース管理に適していることは間違いありませんが、何らかの形でメモリの割り当てと割り当て解除を使用しているという事実は変わりません。この質問はもっと低レベルだと思います。
5gon12eder 2016年

3

あるスレッドから別のスレッドにオブジェクトを通信するときの最大のパフォーマンスヒットは、ロックを取得するオーバーヘッドです。これは、数マイクロ秒のオーダーであり、new/のペアがdelete取る平均時間(100ナノ秒のオーダー)を大幅に上回ります。健全なnew実装は、ほぼすべてのコストでロックを回避して、パフォーマンスへの影響を回避しようとします。

つまり、あるスレッドから別のスレッドにオブジェクトを通信するときにロックを取得する必要がないようにする必要があります。これを達成するための2つの一般的な方法を知っています。どちらも、1つの送信者と1つの受信者の間で一方向にのみ機能します。

  1. リングバッファを使用してください。どちらのプロセスも、このバッファーへの1つのポインターを制御します。1つは読み取りポインター、もう1つは書き込みポインターです。

    • 送信側はまず、ポインターを比較して要素を追加する余地があるかどうかを確認し、次に要素を追加してから、書き込みポインターをインクリメントします。

    • レシーバーは、ポインターを比較して、読み取る要素があるかどうかを確認し、要素を読み取ってから、読み取りポインターをインクリメントします。

    ポインターはスレッド間で共有されるため、アトミックである必要があります。ただし、各ポインタは1つのスレッドによってのみ変更され、もう1つのスレッドはポインタへの読み取りアクセスのみを必要とします。バッファー内の要素はそれ自体がポインターである場合があります。これにより、リングバッファーのサイズを送信者のブロックにならないサイズに簡単に設定できます。

  2. 常に少なくとも1つの要素を含むリンクリストを使用します。受信側には最初の要素へのポインターがあり、送信側には最後の要素へのポインターがあります。これらのポインタは共有されません。

    • 送信者は、リンクリストの新しいノードを作成し、そのnextポインターをに設定しますnullptr。次にnext、新しい要素を指すように最後の要素のポインタを更新します。最後に、新しい要素を独自のポインタに格納します。

    • レシーバーnextは、最初の要素のポインターを監視して、利用可能な新しいデータがあるかどうかを確認します。もしそうなら、それは古い最初の要素を削除し、それ自身のポインタを現在の要素を指すように進め、それの処理を開始します。

    この設定では、nextポインターはアトミックである必要があり、送信者はnextポインターを設定した後、最後から2番目のエレメントを逆参照しないようにする必要があります。もちろん、利点は、送信者がブロックする必要がないことです。

どちらのアプローチも、ロックベースのアプローチよりもはるかに高速ですが、正しく行うには慎重な実装が必要です。そしてもちろん、それらにはネイティブのハードウェアのポインター書き込み/ロードの原子性が必要です。atomic<>実装が内部でロックを使用する場合、あなたはかなり運命にあります。

同様に、リーダーやライターが複数いる場合は、かなり運命にあります。ロックのないスキームを考え出そうとするかもしれませんが、実装するのは難しいです。これらの状況は、ロックで処理する方がはるかに簡単です。ただし、ロックを取得すると、new/ deleteパフォーマンスの心配をやめることができます。


+1 CASループを使用する同時キューの代わりに、このリングバッファーソリューションをチェックアウトする必要があります。それはとても有望に聞こえます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.