【问题标题】:Is there a flat unsorted map/set implementation?是否有平面未排序的地图/集合实现?
【发布时间】:2015-09-05 10:15:42
【问题描述】:

boost.container flat_map 等,还有 Loki AssocVector 和许多其他类似的东西,它们保持元素排序。

是否有现代(支持 c++11 移动等)的未排序矢量的实现,适用于地图/集?

这个想法是将它用于非常小的地图/集合(少于 20 个元素)和简单的键(散列并不总是有意义)

【问题讨论】:

  • 我不确定你到底想要什么。为什么将它存储为平面(即向量)很重要?要将未排序的向量用作地图/集合,您需要一些排序索引,您是否打算将其保留在容器外部?
  • @onqtam,但是您的键将是未排序的,您必须进行线性搜索,并且擦除元素仍然需要重新排列元素。如果可以接受,请使用std::vector(请参阅我的回答)。
  • 如果您担心缓存局部性,您应该将键和值保存在不同的向量中,除非所有内容都适合 L1 缓存。
  • @onqtam 也许您需要“unordered_map”的模拟?或者只是带有 O(n) 查找的向量?我猜你要的是hash table with open addressing - 即存储数组中所有元素的哈希表,如vector<optional<T>>
  • @onqtam 有不同类型的哈希映射,请阅读上面的链接 - 开放寻址所有哈希表的元素都驻留在单个数组中。

标签: c++ c++11 boost stl containers


【解决方案1】:

这样的?

template<class Key, class Value, template<class...>class Storage=std::vector>
struct flat_map {
  struct kv {
    Key k;
    Value v;
    template<class K, class V>
    kv( K&& kin, V&& vin ):k(std::forward<K>(kin)), v(std::forward<V>(vin)){}
  };
  using storage_t = Storage<kv>;
  storage_t storage;

  // TODO: adl upgrade
  using iterator=decltype(std::begin(std::declval<storage_t&>()));
  using const_iterator=decltype(std::begin(std::declval<const storage_t&>()));
  // boilerplate:
  iterator begin() {
    using std::begin;
    return begin(storage);
  }
  const_iterator begin() const {
    using std::begin;
    return begin(storage);
  }
  const_iterator cbegin() const {
    using std::begin;
    return begin(storage);
  }
  iterator end() {
    using std::end;
    return end(storage);
  }
  const_iterator end() const {
    using std::end;
    return end(storage);
  }
  const_iterator cend() const {
    using std::end;
    return end(storage);
  }
  size_t size() const {
    return storage.size();
  }
  bool empty() const {
    return storage.empty();
  }
  // these only have to be valid if called:
  void reserve(size_t n) {
    storage.reserve(n);
  }
  size_t capacity() const {
    return storage.capacity();
  }
  // map-like interface:
  // TODO: SFINAE check for type of key
  template<class K>
  Value& operator[](K&& k){
    auto it = find(k);
    if (it != end()) return it->v;
    storage.emplace_back( std::forward<K>(k), Value{} );
    return storage.back().v;
  }
private: // C++14, but you can just inject the lambda at point of use in 11:
  template<class K>
  auto key_match( K& k ) {
    return [&k](kv const& kv){
      return kv.k == k;
    };
  }
public:
  template<class K>
  iterator find(K&& k) {
    return std::find_if( begin(), end(), key_match(k) );
  }
  template<class K>
  const_iterator find(K&& k) const {
    return const_cast<flat_map*>(this)->find(k);
  }
  // iterator-less query functions:
  template<class K>
  Value* get(K&& k) {
    auto it = find(std::forward<K>(k));
    if (it==end()) return nullptr;
    return std::addressof(it->v);
  }
  template<class K>
  Value const* get(K&& k) const {
    return const_cast<flat_map*>(this)->get(std::forward<K>(k));
  }
  // key-based erase: (SFINAE should be is_comparible, but that doesn't exist)
  template<class K, class=std::enable_if_t<std::is_converible<K, Key>{}>>
  bool erase(K&& k) {
    auto it = std::remove(
      storage.begin(), storage.end(), key_match(std::forward<K>(k))
    );
    if (it == storage.end()) return false;
    storage.erase( it, storage.end() );
    return true;
  }
  // classic erase, for iterating:
  iterator erase(const_iterator it) {
    return storage.erase(it);
  }
  template<class K2, class V2,
    class=std::enable_if_t<
      std::is_convertible< K2, Key >{}&&
      std::is_convertible< V2, Value >{}
    >
  >
  void set( K2&& kin, V2&& vin ) {
    auto it = find(kin);
    if (it != end()){
      it->second = std::forward<V2>(vin);
      return;
    } else {
      storage.emplace_back( std::forward<K2>(kin), std::forward<V2>(vin) );
    }
  }
};

我将容器类型保留为模板参数,因此您可以选择使用类似 SBO 向量的结构。

理论上,我应该公开一个模板参数来替换键上的等号。不过,我确实让键搜索功能变得透明。

【讨论】:

  • @nwp 不,这个问题要求一个未排序的向量,你误读了。我对键和值使用相同的向量,因为保持连续性通常值得付出一点对齐的代价。
  • 哇!你刚写的吗?它在修复is_converible 中的错字后起作用。它还可以与 boost SBO 容器 template &lt;class T&gt; using mySmallVec = boost::container::small_vector&lt;T, 5&gt;; 一起使用。这门课还缺少什么?你会考虑在某个地方的回购中公开它吗?
  • 我只是想知道为什么其他容器(如boost::container::flat_map)不提供内部容器类型作为模板参数......会非常有用!
  • 如果我使用boost::container::static_vector,我什至可以 memcpy 整个地图(如果键和值是 POD) - 我认为它在内部不使用指针(但我必须检查那...)
  • 你为什么不使用一对,而是为键/值对定义你自己的结构?你在哪里写了// TODO: SFINAE check for type of key 你的意思是使用enable_if 类似erase 的东西吗? set 的目的是什么 - 比使用 map.[key] = rvalue 更优化?如果是这样,为什么std::map 没有这样的类比?而且 emplace 不是优于 set,因为它转发变量参数吗?
【解决方案2】:

如果集合肯定很小,那么您可以使用std::vector(或std::deque)并使用线性搜索进行查找。在小向量上进行 O(n) 线性搜索比在红黑树等更复杂的结构上进行 O(log(n)) 搜索要快。

所以你可以将元素放在vector 中而不对它们进行排序。如果您删除元素,您仍然需要进行一些改组,但是对于平面向量状结构,无论它是否已排序,这始终是正确的,除非您只删除后元素。为什么它是平坦的很重要?

【讨论】:

  • 平面化以避免分配和缓存局部性很重要。
  • 那么你必须在洗牌上妥协。你不能同时拥有两者,除非你实际上并没有删除元素,只是以某种方式将它们标记为死元素,然后在下一次插入时重新使用死元素。
  • @JonathanWakely 从未排序的向量中移除元素是 O(1) - 只需将其与 .back() 交换,然后与 .pop_back() 交换。
  • 不要喝缓存位置库尔援助...除非您可以将代码分析到缓存未命中的级别并证明您的地图查找是导致 unordered_map 是最佳默认选择的原因。
  • @EvgenyPanasyuk,是的,但是交换仍然是洗牌,即使一次只换一个元素
【解决方案3】:

std::unordered_setstd::unordered_map,但据我所知,它们不是使用向量实现的。

一个可能的选择是编写自己的哈希向量并使用std::hash&lt;Key&gt; 对密钥进行哈希处理,然后索引结果数字以向量的长度为模,但随后您必须找到一种处理冲突和所有问题的方法手动产生的问题。不确定我是否推荐过。

另一种方法是将自定义分配器传递给std::unordered_setstd::unordered_map,它们在向量上执行分配(例如,通过拥有一个内部向量),正如@BeyelerStudios 所建议的那样。

【讨论】:

  • 你可以给你的 unordered_map 一个基于向量的分配器
  • @BeyelerStudios,除非它具有预先分配的固定最大大小,否则这样的分配器将无法正常工作,在这种情况下,您不妨只使用数组。如果它没有固定的最大大小,那么当它增长时,它需要重新分配和移动所有元素,这违反了unordered_map 的引用稳定性保证。插入新元素不得使对现有元素的引用无效。
  • @JonathanWakely 是的,听起来就是这种情况(固定的最大大小) - 几乎每个遇到的问题都有一个固定的最大大小(您可以计算)
  • @BeyelerStudios 但如果您有固定的最大尺寸,为什么要使用矢量而不是 unique_ptr&lt;unsigned char[]&gt;,后者更简单且开销更少?
  • @JonathanWakely 这有点过时了:如果输入/API 已经需要向量怎么办?
【解决方案4】:

Evgeny Panasyuk 是正确的,我相信您想要的是开放地址哈希映射
这完全符合您的要求,只有 1 个平面缓冲区,没有分配节点,没有要跟随的指针,并且未排序。

否则,您也有 flat_map/AssocVector,但它们已排序,与您的要求不同。

对于 OAHM,我在这里有一个类似 STL 的通用实现:
https://sourceforge.net/projects/cgenericopenaddresshashmap/

另外你可能想看看flat_map的基准页面:
boost::flat_map and its performance compared to map and unordered_map
OAHM 在所有测试中的表现都非常接近flat_map,迭代除外。

【讨论】:

    【解决方案5】:

    请看我最近更新到GitHub的sfl库https://github.com/slavenf/sfl-library

    它是 C++11 仅标头库,提供平面有序和无序容器,可将元素连续存储在内存中。所有容器均满足ContainerAllocatorAwareContainerContiguousContainer 的要求。库在 zlib 许可下获得许可。

    【讨论】:

      猜你喜欢
      • 2013-03-21
      • 1970-01-01
      • 2013-01-16
      • 1970-01-01
      • 1970-01-01
      • 2015-06-06
      • 2010-11-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多