夜は良きアドバイザーであり、アイデアがあるかもしれないと思います ;)
- 最近では、すべてのデータが L1 キャッシュに収まる場合、メモリは CPU よりもはるかに遅くなりますが、L2 または L3 に簡単にあふれてしまいます。少なくとも 3 つのポインター + オブジェクト (つまり、32 ビット マシンでは少なくとも 16 バイト、64 ビット マシンでは 32 バイト) => 少なくとも 80k のメモリであり、最近の CPU は L1D 用に 32k しかないため、すでにあふれています。 L2に
- 前の事実は、セット ノードがおそらくメモリ内に散らばっており、密集していないという問題によって悪化しています。つまり、キャッシュ ラインの一部がまったく無関係なもので満たされていることを意味します。これは、ノードを互いに近くに保つアロケーターを提供することで軽減できます。
- そしてこれは、CPU がランダム読み取りよりもシーケンシャル読み取り (メモリが必要になる前にプリフェッチできるため、それを待つ必要がない) の方がはるかに優れているという事実によってさらに悪化します (残念ながら、ツリー構造は非常にランダムな読み取りにつながります)。読む)
これが、速度が重要な場合、 a vector
(またはおそらく a deque
) が非常に優れた構造である理由です。それらはメモリで非常にうまく機能します。vector
そのため、中間構造として使用することを強くお勧めします。ただし、再配置を避けるために、末端からの挿入/削除のみに注意する必要があります。
そこで、かなり単純なアプローチを考えました:
#include <cassert>
#include <algorithm>
#include <set>
#include <vector>
// Do not call this method if you have a single set...
// And the pointers better not be null either!
std::vector<int> intersect(std::vector< std::set<int> const* > const& sets) {
for (auto s: sets) { assert(s && "I said no null pointer"); }
std::vector<int> result; // only return this one, for NRVO to kick in
// 0. Check obvious cases
if (sets.empty()) { return result; }
if (sets.size() == 1) {
result.assign(sets.front()->begin(), sets.front()->end());
return result;
}
// 1. Merge first two sets in the result
std::set_intersection(sets[0]->begin(), sets[0]->end(),
sets[1]->begin(), sets[1]->end(),
std::back_inserter(result));
if (sets.size() == 2) { return result; }
// 2. Merge consecutive sets with result into buffer, then swap them around
// so that the "result" is always in result at the end of the loop.
std::vector<int> buffer; // outside the loop so that we reuse its memory
for (size_t i = 2; i < sets.size(); ++i) {
buffer.clear();
std::set_intersection(result.begin(), result.end(),
sets[i]->begin(), sets[i]->end(),
std::back_inserter(buffer));
swap(result, buffer);
}
return result;
}
正しいようですが、明らかにその速度は保証できません。