だから私は基本的に巨大なデータセットファイルを解析してメモリのハッシュマップにコンテンツをロードするこのC++プログラムを持っています(この部分はメインスレッドでスロットルされています、それで膨大な時間を費やすために邪魔になることはありません)。そして、それが完了したら、新しいメモリの場所へのポインタを反転し、古い場所で削除を呼び出します。それ以外は、プログラムは(メインスレッドで)メモリマップ内のコンテンツを検索して、着信要求のマッチングを実行しています。これらの巨大なマップがEvaluator
クラスにラップされていると仮定します。
Evaluator* oldEvaluator = mEvaluator;
Evaluator* newEvaluator = parseDataSet();
mEvaluator = newEvaluator;
delete oldEvaluator;
//And then on request processing:
mEvaluator.lookup(request)
マップには、keysとして数百万の文字列オブジェクトを含めることができます。これらは、ip、UserAgentなどのリクエスト属性である可能性のある通常の文字列ですが、それぞれがSTL unordered_mapに挿入された文字列オブジェクトです。
データセットは定期的に更新されますが、ほとんどの場合、プログラムはメモリ内のデータセットに対してリクエスト属性のマッチングを行っているだけであり、新しいデータセットの大量消費が発生した場合を除いて、エラーは発生しません。この大きなデータセットを使用する別の方法は、streamingを使用することですが、これは比較的長期的なソリューションです。
以前はイベントドリブンモデルを使用するシングルスレッドプログラムでしたが、完全に新しいセットが配置されて破棄が呼び出されるたびに、全体を削除してリクエスト処理をブロックするのに時間がかかりすぎました。
そのため、このようなマップの削除を別のスレッドに置きました。問題は、削除と要求処理が同時に発生しているように見える一方で、要求処理スレッドで非常に目に見える、大幅な速度低下が見られることです。
もちろん、ホスト上で実行されている他のプロセスがあり、私は2つのスレッドがCPUサイクルで競合することを期待しています。しかし、リクエストマッチングスレッドが大幅に遅くなるとは思っていませんでした。平均して、リクエストは500usレベルで処理されるはずですが、削除スレッドの実行中に5msほどの速度になりました。場合によってはcpuが一致するスレッドに割り込みます(時間がかかりすぎたため)、50ミリ秒、120ミリ秒など長くなる場合があります。極端な場合、リクエスト全体が処理されるまでに1000ミリ秒かかる可能性があります。データ構造の削除には別のスレッドが使用されます。
そのようなスローダウンの根本的な原因を知る最良の方法は何ですか? それは、CPUまたはメモリ帯域幅のボトルネックの方が多いですか?別のスレッドに配置している限り、文字列オブジェクトを1つずつ削除する必要があるので、他のスレッドに影響を与えるとは思っていませんでした。
[〜#〜] edit [〜#〜]:いくつかのコメント/回答のおかげで、いくつかの考えられる原因がすでに指摘されているようです:
それでは、どうしたらいいですか?私はJemalloc
を試しましたが、完全に正しく使用するかどうかはわかりません--- -ljemalloc
リンカー行で、魔法のようにlibcのmallocを置き換えますか?パフォーマンスの違いはありませんでしたが、間違って使用している可能性があります。私のプログラムは明示的なmallocを実行せず、すべてが事前にサイズが不明なnew
であり、ポインターおよびSTLマップと一緒に接続されています。
また、Keyに格納されているすべての文字列は、クイックルックアップに特別に使用されるため、連続したメモリ空間を作成する場合でも、インデックス付きのベクターに格納できません、それらを見つけるのは恐ろしいでしょう。そう、
Jemalloc
がこれのいずれかを解決するかどうかはわかりません)与えられたすべての回答とコメントのおかげで、部分的に問題自体が曖昧であり、単一の回答で実際にすべてをカバーすることはできなかったため、私は最良のものを選ぶことができませんでした。しかし、私はこれらの答えから多くを学び、それゆえそれらのほとんどを支持しました。さまざまな実験の結果、主な問題は次のとおりです。
削除スレッドでの低速な操作が別の影響を与える理由。両方のスレッドで同時にmalloc/deallocを実行しないので、ヒープの競合や、ボトルネックでの一般的なCPUや使用可能なメモリがないはずです。残っているもっともらしい説明はメモリ帯域幅の枯渇です。私は発見しました 別の投稿へのこの回答 は言う:it's generally possible for a single core to saturate the memory bus if memory access is all it does.
私の削除スレッドが実行するのは、巨大なマップをトラバースし、その中の各要素を削除することだけなので、メモリバスを飽和させて他のスレッドにすると考えられます。メモリアクセスと他の計算の両方を実行しているため、速度が大幅に低下します。ここからは、この削除が遅くなる可能性があるさまざまな理由に焦点を当てます
マップは巨大です、数百万の要素と数百メガバイトのサイズ。それらのすべてを削除するには、最初にそれらにアクセスする必要があり、明らかにL1/L2/L3キャッシュに収まるものはほとんどありません。したがって、大量のキャッシュミスとRAMからのフェッチがあります。
ここで言及したいくつかの回答/コメントとして、私はstd::string
オブジェクトをマップに格納します。それぞれに独自のスペースが割り当てられており、1つずつフェッチして削除する必要があります。 MSaltersからのアドバイスマップにstring_view
を格納することにより、パフォーマンスが大幅に向上します一方で、各文字列の実際のバイトコンテンツを、事前に割り当てられた連続したメモリブロックに格納します。現在、マップ内の100万個のオブジェクトの削除は、単なるポインタであるstring_view
オブジェクトのほとんど些細な破壊になり、すべての文字列コンテンツの破壊は、事前に割り当てられたブロックの破壊です。
プログラムの他の部分では、他のC++オブジェクトも他のマップに格納することについては触れませんでした。そして、同様に問題があります。 string_view
のような既成のクラスがないと実行が困難ですが、このようなC++オブジェクトの同様の「フラット化」が必要です。考えられるのはプリミティブ型とポインタをできるだけ多く保存するであり、すべてのコンテンツ(ほとんどは文字列にまとめることができる)を連続したバイトバッファに入れることです。 すべてを簡単に破壊することが目標です。
最後に、特にそれが大きい場合、マップコンテナー自体を破棄するにはかなりのコストがかかる可能性があることがわかります。 Node-based の場合、各ノードハンドルをトラバースおよび削除するstdコンテナーには時間がかかります。私が見つけたのは本当にフラットなハッシュマップの代替実装であり、削除がはるかに速くなりますです。このようなマップの例には、 Abseil flat_hash_map および このブロガーのflat_hash_map が含まれます。それらはフラットですが、どちらも真のhash_mapであることに注意してください。 Boostの flat_map も非常に高速に削除できますが、これは実際のhashMapではありません。厳密に順序付けされたベクトルに支えられているため、(入力が順序付けられていない場合)挿入が非常に遅くなります。