web-dev-qa-db-ja.com

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

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

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

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

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

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

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

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

9
Philipp Claßen

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

上限が予想できる場合char buf[256]、例:まれなケースでヒープ割り当てのみを呼び出すことができない場合の実用的な代替方法:

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

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

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

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

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

3
gbjbaanb

あるスレッドから別のスレッドにオブジェクトを通信するときの最大のパフォーマンスヒットは、ロックを取得するオーバーヘッドです。これは数マイクロ秒程度であり、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のパフォーマンスについて心配する必要がなくなります。

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

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

次に、新しいものを置き換えてAllocMessage<T>およびfreeMessage<T>で削除するだけで、リソースプーリングを簡単に実行できます。私の提案は、具体的なTを割り当てるときに、messagesが持つことができる潜在的なサイズの量を制限し、切り上げることです。

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

3
ratchet freak