web-dev-qa-db-ja.com

一意の値を格納するコンテナは何ですか?

次の問題があります。私は毎秒平均60フレームで動作するゲームを持っています。各フレームはコンテナに値を格納する必要があり、重複があってはなりません。

おそらくフレームごとに100未満のアイテムを格納する必要がありますが、挿入呼び出しの数ははるかに多くなります(また、一意である必要があるため、多くは拒否されます)。フレームの終わりでのみ、コンテナをトラバースする必要があります。したがって、フレームごとにコンテナを約60回繰り返しますが、挿入数は多くなります。

保存するアイテムは単純な整数であることに注意してください。

これに使えるコンテナはたくさんありますが、何を選べばいいのかわからないです。これにはパフォーマンスが重要な問題です。

私が集めたいくつかの賛否両論:


ベクトル

  • (PRO):連続した記憶、大きな要因。
  • (PRO):メモリを最初に予約でき、その後の割り当て/割り当て解除はほとんどありません
  • (CON):コンテナ(std :: find)をトラバースして各insert()をトラバースして一意のキーを見つける以外に方法はありませんか?比較は簡単ですが(整数)、コンテナ全体がおそらくキャッシュに収まる可能性があります

セット

  • (PRO):シンプルで、明らかにこれを意味します
  • (CON):挿入時間が一定ではない
  • (CON):フレームごとの多くの割り当て/割り当て解除
  • (CON):連続したメモリではありません。何百ものオブジェクトのセットをトラバースすることは、メモリ内をたくさん飛び回ることを意味します。

nordered_set

  • (PRO):シンプルで、明らかにこれを意味します
  • (PRO):平均ケース定数時間挿入
  • (CON):整数を格納しているので、ハッシュ操作はおそらく他の何よりもかなり高価です
  • (CON):フレームごとの多くの割り当て/割り当て解除
  • (CON):連続したメモリではありません。何百ものオブジェクトのセットをトラバースすることは、メモリ内をたくさん飛び回ることを意味します。

Setは明らかにこの問題を対象としていますが、メモリアクセスパターンのために、ベクタールートを使用することに傾倒しています。私にはわからない大きな問題は、挿入ごとにベクトルをトラバースする方が、割り当て/割り当て解除(特にこれを実行する必要がある頻度を考慮する)やセットのメモリルックアップよりもコストがかかるかどうかです。

最終的には、すべてが各ケースのプロファイリングに帰着することを私は知っていますが、ヘッドスタートとして、または単に理論的に他ならないとしたら、このシナリオでおそらく何が最善でしょうか?私も見逃したかもしれない賛否両論はありますか?

編集:私が言及しなかったように、コンテナは各フレームの終わりにcleared()されます

13
KaiserJohaan

ここで首をブロックに置き、サイズが100で、格納されているオブジェクトが整数値である場合、ベクトルルートがおそらく最も効率的であることを提案します。これの単純な理由は、setとunordered_setが各挿入にメモリを割り当てるのに対し、ベクトルは複数回必要ないためです。

ベクトルの順序を維持することで、検索パフォーマンスを劇的に向上させることができます。これにより、すべての検索がバイナリ検索になり、log2N時間で完了するためです。

欠点は、メモリの移動により挿入にかかる時間がわずかに長くなることですが、挿入よりも検索数が多いように聞こえ、(平均)50個の連続するメモリワードを移動するのはほぼ瞬時の操作です。

最後に:正しいロジックを今すぐ書いてください。ユーザーが不満を言っているときのパフォーマンスについて心配します。

編集:私は自分自身を助けることができなかったので、ここにかなり完全な実装があります:

template<typename T>
struct vector_set
{
    using vec_type = std::vector<T>;
    using const_iterator = typename vec_type::const_iterator;
    using iterator = typename vec_type::iterator;

    vector_set(size_t max_size)
    : _max_size { max_size }
    {
        _v.reserve(_max_size);
    }

    /// @returns: pair of iterator, bool
    /// If the value has been inserted, the bool will be true
    /// the iterator will point to the value, or end if it wasn't
    /// inserted due to space exhaustion
    auto insert(const T& elem)
    -> std::pair<iterator, bool>
    {
        if (_v.size() < _max_size) {
            auto it = std::lower_bound(_v.begin(), _v.end(), elem);
            if (_v.end() == it || *it != elem) {
                return make_pair(_v.insert(it, elem), true);
            }
            return make_pair(it, false);
        }
        else {
            return make_pair(_v.end(), false);
        }
    }

    auto find(const T& elem) const
    -> const_iterator
    {
        auto vend = _v.end();
        auto it = std::lower_bound(_v.begin(), vend, elem);
        if (it != vend && *it != elem)
            it = vend;
        return it;
    }

    bool contains(const T& elem) const {
        return find(elem) != _v.end();
    }

    const_iterator begin() const {
        return _v.begin();
    }

    const_iterator end() const {
        return _v.end();
    }


private:
    vec_type _v;
    size_t _max_size;
};

using namespace std;


BOOST_AUTO_TEST_CASE(play_unique_vector)
{
    vector_set<int> v(100);

    for (size_t i = 0 ; i < 1000000 ; ++i) {
        v.insert(int(random() % 200));
    }

    cout << "unique integers:" << endl;
    copy(begin(v), end(v), ostream_iterator<int>(cout, ","));
    cout << endl;

    cout << "contains 100: " << v.contains(100) << endl;
    cout << "contains 101: " << v.contains(101) << endl;
    cout << "contains 102: " << v.contains(102) << endl;
    cout << "contains 103: " << v.contains(103) << endl;
}
7
Richard Hodges

私は、候補と思われるいくつかの異なる方法でタイミングを取りました。 std::unordered_setを使用することが勝者でした。

これが私の結果です:

 UnorderedSetの使用:0.078s 
 UnsortedVectorの使用:0.193s 
 OrderedSetの使用:0.278s 
 SortedVectorの使用:0.282s 

タイミングは、各ケースの5回の実行の中央値に基づいています。

コンパイラ:gccバージョン4.9.1 
フラグ:-std = c ++ 11 -O2 
 OS:ubuntu 4.9.1 
 CPU:Intel(R) Core(TM)i5-4690K CPU @ 3.50GHz 

コード:

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <random>
#include <set>
#include <unordered_set>
#include <vector>

using std::cerr;
static const size_t n_distinct = 100;

template <typename Engine>
static std::vector<int> randomInts(Engine &engine,size_t n)
{
  auto distribution = std::uniform_int_distribution<int>(0,n_distinct);
  auto generator = [&]{return distribution(engine);};
  auto vec = std::vector<int>();
  std::generate_n(std::back_inserter(vec),n,generator);
  return vec;
}


struct UnsortedVectorSmallSet {
  std::vector<int> values;
  static const char *name() { return "UnsortedVector"; }
  UnsortedVectorSmallSet() { values.reserve(n_distinct); }

  void insert(int new_value)
  {
    auto iter = std::find(values.begin(),values.end(),new_value);
    if (iter!=values.end()) return;
    values.Push_back(new_value);
  }
};


struct SortedVectorSmallSet {
  std::vector<int> values;
  static const char *name() { return "SortedVector"; }
  SortedVectorSmallSet() { values.reserve(n_distinct); }

  void insert(int new_value)
  {
    auto iter = std::lower_bound(values.begin(),values.end(),new_value);
    if (iter==values.end()) {
      values.Push_back(new_value);
      return;
    }
    if (*iter==new_value) return;
    values.insert(iter,new_value);
  }
};

struct OrderedSetSmallSet {
  std::set<int> values;
  static const char *name() { return "OrderedSet"; }
  void insert(int new_value) { values.insert(new_value); }
};

struct UnorderedSetSmallSet {
  std::unordered_set<int> values;
  static const char *name() { return "UnorderedSet"; }
  void insert(int new_value) { values.insert(new_value); }
};



int main()
{
  //using SmallSet = UnsortedVectorSmallSet;
  //using SmallSet = SortedVectorSmallSet;
  //using SmallSet = OrderedSetSmallSet;
  using SmallSet = UnorderedSetSmallSet;

  auto engine = std::default_random_engine();

  std::vector<int> values_to_insert = randomInts(engine,10000000);
  SmallSet small_set;
  namespace chrono = std::chrono;
  using chrono::system_clock;
  auto start_time = system_clock::now();
  for (auto value : values_to_insert) {
    small_set.insert(value);
  }
  auto end_time = system_clock::now();
  auto& result = small_set.values;

  auto sum = std::accumulate(result.begin(),result.end(),0u);
  auto elapsed_seconds = chrono::duration<float>(end_time-start_time).count();

  cerr << "Using " << SmallSet::name() << ":\n";
  cerr << "  sum=" << sum << "\n";
  cerr << "  elapsed: " << elapsed_seconds << "s\n";
}
8
Vaughn Cato

挿入が多く、トラバーサルが1つしかないということですが、ベクターを使用して、ベクター内で一意であるかどうかに関係なく、要素をプッシュすることをお勧めします。これはO(1)で行われます。

ベクトルを調べる必要がある場合は、それを並べ替えて、重複する要素を削除します。有界整数であるため、これはO(n)で実行できると思います。

[〜#〜] edit [〜#〜]カウントソートで線形時間でソート このビデオ で表示されます。実行できない場合は、O(n lg(n))に戻ります。

メモリ内のベクトルが隣接しているため、キャッシュミスはほとんど発生せず、割り当てもほとんどありません(特に、ベクトルに十分なメモリを予約している場合)。

2
qdii