【问题标题】:What container to store unique values?什么容器来存储唯一值?
【发布时间】:2015-04-30 07:01:41
【问题描述】:

我遇到了以下问题。我有一个平均每秒运行 60 帧的游戏。每一帧我都需要将值存储在一个容器中,并且不能有重复项。

它可能每帧必须存储少于 100 个项目,但插入调用的数量会更多(并且由于它必须是唯一的而许多被拒绝)。只有在帧的末尾我才需要遍历容器。因此,每帧大约有 60 次容器迭代,但插入次数更多。

记住要存储的项目是简单的整数。

我可以使用很多容器,但我无法决定选择什么。性能是其中的关键问题。

我收集的一些优点/缺点:


矢量

  • (PRO):连续内存,一个巨大的因素。
  • (PRO):可以先保留内存,之后很少分配/释放
  • (CON):除了遍历容器 (std::find) 每个 insert() 来查找唯一键之外,别无选择?比较很简单(整数),整个容器可能适合缓存

设置

  • (专业版):简单,明确的目的
  • (CON):插入时间不固定
  • (CON):每帧有很多分配/解除分配
  • (CON):非连续内存。遍历一组数百个对象意味着在内存中跳来跳去。

unordered_set

  • (专业版):简单,明确的目的
  • (PRO):平均大小写常数时间插入
  • (CON):鉴于我存储整数,散列操作可能比其他任何操作都贵很多
  • (CON):每帧有很多分配/解除分配
  • (CON):非连续内存。遍历一组数百个对象意味着在内存中跳来跳去。

由于内存访问模式,我倾向于使用向量路由,即使 set 显然是针对这个问题的。我不清楚的一个大问题是遍历每个插入的向量是否比分配/释放(特别是考虑到必须执行的频率)和 set 的内存查找成本更高。

我知道最终这一切都归结为对每个案例进行剖析,但如果只是作为先发制人或只是理论上,那么在这种情况下什么可能是最好的?是否还有我可能遗漏的优点/缺点?

编辑:正如我没有提到的,容器在每一帧结束时被清除()

【问题讨论】:

  • 只需测量即可。 鉴于unordered_set 经典的“集合”容器,无序- 重复语义和最佳渐近复杂度,我会试一试,但 vector 很有可能会在小型容器中击败它,因为它具有更好的缓存位置属性。
  • 如何提供自己的分配器,以克服内存管理效率低下的问题? (例如提供对象池)
  • 无论你做什么,试着正确地封装你的代码并使用auto来跟踪类型,这样你以后就可以轻松地改变你对容器的选择。然后测量。
  • 如果您知道向量中要存储的值的范围,您可以将它们放在自己的向量中,然后在容器上使用random_suffle,并从中获取前 100 个元素。
  • A set 可能会受到分支错误预测的支配。完整的数据适合 L1 并且可以避免分配(块分配器)。但是,set 是一个树形结构,因此每个插入的错误预测概率约为 50% 的多个分支是不可避免的。

标签: c++ memory-management containers


【解决方案1】:

我使用了几种不同的方法进行计时,我认为这些方法可能是候选方法。使用std::unordered_set 是赢家。

这是我的结果:

使用无序集:0.078s 使用 UnsortedVector:0.193s 使用 OrderedSet:0.278s 使用 SortedVector:0.282s

时间基于每个案例五次运行的中位数。

编译器:gcc 版本 4.9.1 标志:-std=c++11 -O2 操作系统: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";
}

【讨论】:

    【解决方案2】:

    我将把我的脖子放在块上,并建议当大小为 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;
    }
    

    【讨论】:

    • 不管怎样,如果你已经在使用 Boost,这种容器可以在 Boost.Container 库中作为 flat_set 使用。它还有一个flat_map
    • 好点!我很惊讶我没有想到先看升压。从 c++14 开始我似乎忘记了如何使用 boost...
    【解决方案3】:

    正如您所说,您有很多插入并且只有一次遍历,我建议使用向量并将元素推入,无论它们在向量中是否唯一。这是在O(1) 中完成的。

    就在你需要遍历向量的时候,然后对其进行排序并移除重复的元素。我相信这可以在O(n) 中完成,因为它们是有界整数。

    编辑:通过this video 中提出的计数排序 以线性时间排序。如果不可行,则返回O(n lg(n))

    由于向量在内存中的连续性和很少的分配(特别是如果您在向量中保留足够的内存),您将很少有缓存未命中。

    【讨论】:

      猜你喜欢
      • 2016-01-25
      • 1970-01-01
      • 2018-08-20
      • 1970-01-01
      • 2010-09-30
      • 1970-01-01
      • 1970-01-01
      • 2011-06-07
      • 2016-07-07
      相关资源
      最近更新 更多