• [C++]怎么样实现一个较快的Hash Table


      我们服务器一直在用boost/sgl stl的hash table,但是从来没有考虑过其中的效率问题,虽然hash_map/unordered_map跑的可能真的比map快一些,可能应该不是你理解的那么快.其实他可以更快一些!!!

      当我自己尝试着实现了一个hash table之后,我发现确实如此.这篇文章也是来说说,如何实现较快的一个.

      通常的hash table都是用开链法,开放地址法来解决冲突.开链法是总容易实现的一个,而且因为效率稳定,被加入了C++11,取名unordered_map.不过效率实在不咋地.

      开放地址法的hash table,我是从google-sparsehash里面注意到的,虽然数据结构,算法导论都会讲到.网上说速度很快,我就去看了一下API,其比普通的unordered_map多了一组API:

    1.  set_empty_key/set_deleted_key

      在开链法中,所有的节点都是容器内的内容,可是开放地址法中不是的.所以需要额外的信息来维护节点的可用性信息.

      当时我看到这两个API,大概就猜到内存是怎么实现的,闲来无事就是试着写了一个demo,在VC 2008下面跑的结果是,比unordered_map快一倍多;在Linux x64 gcc 4.4下面的结果是,比unordered_map快了将近1倍.

    2. 高性能的hash table必须是开放地址法的

      这么说,是有原因的.链表的特性就是容易删除,容易插入.可是hash table不需要这些特性,hash table只需要快.可以链表这东西,偏偏做不到快速定位,虽然你知道有下一个节点,但是你不知道下一个节点的准确位置,经常会造成缓存未命中,浪费大量时间.

    3. bucket的容量

      bucket的容量也是影响hash table性能的一个因素.无数的数据结构和算法书籍,都教导大家,通过质数取余数,可以获得比较好的下标分布.可是,无论是除法还是乘法,消耗都是相当高的.十几个或者几十个时钟周期,始终比不上一两个时钟周期快.所以,高性能的hash table必须要把bucket的容量设置成2^n.google-sparsehash里面初始容量是32.扩容的话,都是直接左移;算下标的话,都是(容量-1) & hash_value,简单的一个位运算搞定.

    4. 正确实现find_position

      我自己实现的hash table,是线性探测法的.所以find position也是比较简单,就是通过hash value和掩码,获取到其实下标,然后一个一个test.需要把buckets当作是环形的,否则buckets最末位的数据冲突就会不好搞.(我当时没有考虑这一点,直接给他扩容了.....)

    5. 对象模型

      不同的Key和Value模型,可以导致你对Hash Table的不同实现.简单的说,在C里面,你可以不用考虑Key和Value的生命周期(:D),但是C++里面,你不得不考虑Key,Value的生命周期问题.你不能做一个假设,key和value都是简单数据类型.一个int映射到一个对象,这种经常会用到的.

      所以,erase一个key的时候,需要把key设置成deleted,然后还要把value重置一遍.如果没有重置,对象所引用的内存有可能就会被泄露.

      这引发了我另外一个想法,就是通过模板,来特化Value的reset行为.因为不是所有的Value都是需要被重置的,只有那些复杂对象,才需要.

    6. 可以考虑缓冲hash value

      如果key都是简单数据,而非string或者复杂的数据类型,缓冲是没有任何意义的,因为hash value可以被快速的计算出来;但是当key是char*,或者一些复杂的数据类型,缓冲就会变的有意义.而且缓冲更有利于重排,容器扩容的时候速度会更快一些.

    7. 考虑使用C的内存分配器

      尽量不要使用C++的new/delete来分配内存.new,delete会有对象的构造,析构过程,这可能不是你所希望的.针对key和value数据类型的不同,你可能会有自己的特有的构造,析构过程.而且,C的内存分配器,同样可以被一些第三方库优化,比如tcmalloc/jemalloc等.

    8. 选一个好的Hash函数(这是最重要的)

    9. 尽力防止拷贝

      rehash非常耗时,如果支持C++11,就使用move操作;如果不支持,就用swap,否则会复制很多次.

     代码贴上:

      1 //Copyright 2012, egmkang wang.
      2 // All rights reserved.
      3 //
      4 // Redistribution and use in source and binary forms, with or without
      5 // modification, are permitted provided that the following conditions are
      6 // met:
      7 //
      8 // * Redistributions of source code must retain the above copyright
      9 // notice, this list of conditions and the following disclaimer.
     10 // * Redistributions in binary form must reproduce the above
     11 // copyright notice, this list of conditions and the following disclaimer
     12 // in the documentation and/or other materials provided with the
     13 // distribution.
     14 // * Neither the name of green_turtle nor the names of its
     15 // contributors may be used to endorse or promote products derived from
     16 // this software without specific prior written permission.
     17 //
     18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 //
     30 // author: egmkang (egmkang@gmail.com)
     31 
     32 #ifndef __MY_HASH_TABLE__
     33 #define __MY_HASH_TABLE__
     34 #include <utility>
     35 #include <functional>
     36 #include <cstddef>
     37 #include <stdlib.h>
     38 namespace green_turtle{
     39 
     40 //hash_table with linear probing
     41 template<class Key,
     42         class T,
     43         class Hash = std::hash<Key>,
     44         class KeyEqual = std::equal_to<Key> >
     45 class hash_map
     46 {
     47  public:
     48   typedef Key key_type;
     49   typedef T mapped_type;
     50   typedef std::pair<const Key,T> value_type;
     51   typedef size_t size_type;
     52   typedef Hash hash_fn;
     53   typedef KeyEqual equal_fn;
     54   typedef value_type* iterator;
     55 
     56   hash_map(size_type capacity = 32,key_type empty = key_type(),key_type deleted = key_type()):
     57     empty_key_(empty)
     58     ,deleted_key_(deleted)
     59     ,size_(0)
     60     ,capacity_(capacity)
     61     ,buckets_(nullptr)
     62     ,hasher_()
     63     ,equaler_()
     64   {
     65     init_buckets();
     66   }
     67   ~hash_map()
     68   {
     69     delete_buckets();
     70   }
     71   hash_map(hash_map&& m,size_type capacity = 32):
     72       buckets_(nullptr)
     73   {
     74     empty_key_ = m.empty_key_;
     75     deleted_key_ = m.deleted_key_;
     76     size_ = 0;
     77     capacity_ = m.capacity_;
     78     //to impl the increase and decrease method
     79     if(capacity_ != capacity && capacity >= 32)
     80       capacity_ = capacity;
     81     hasher_ = m.hasher_;
     82     equaler_ = m.equaler_;
     83 
     84     init_buckets();
     85 
     86     copy_from(m);
     87   }
     88   hash_map& operator = (const hash_map& m)
     89   {
     90     empty_key_ = m.empty_key_;
     91     deleted_key_ = m.deleted_key_;
     92     size_ = 0;
     93     capacity_ = m.capacity_;
     94     hasher_ = m.hasher_;
     95     equaler_ = m.equaler_;
     96 
     97     init_buckets();
     98 
     99     copy_from(m);
    100   }
    101   void swap(hash_map& m)
    102   {
    103     std::swap(empty_key_ , m.empty_key_);
    104     std::swap(deleted_key_ , m.deleted_key_);
    105     std::swap(size_ , m.size_);
    106     std::swap(capacity_ , m.capacity_);
    107     std::swap(hasher_ , m.hasher_);
    108     std::swap(equaler_ , m.equaler_);
    109     std::swap(buckets_ , m.buckets_);
    110   }
    111 
    112   iterator end() { return nullptr; }
    113   iterator end() const { return nullptr; }
    114 
    115   iterator find(const key_type& key)
    116   {
    117     if(is_key_empty(key) || is_key_deleted(key))
    118       return NULL;
    119     iterator pair_ = find_position(key);
    120     if(!pair_ || !equaler_(key,pair_->first))
    121       return NULL;
    122     return pair_;
    123   }
    124   iterator find(const key_type& key) const
    125   {
    126     if(is_key_empty(key) || is_key_deleted(key))
    127       return NULL;
    128     iterator pair_ = find_position(key);
    129     if(!pair_ || !equaler_(key,pair_->first))
    130       return NULL;
    131     return pair_;
    132   }
    133 
    134   std::pair<iterator, bool> insert(const value_type& v)
    135   {
    136     std::pair<iterator, bool> result(nullptr, false);
    137     result.first = _insert(v);
    138     result.second = result.first ? true : false;
    139     return result;
    140   }
    141 
    142   template<class P>
    143   std::pair<iterator, bool> insert(P&& p)
    144   {
    145     std::pair<iterator, bool> result(nullptr, false);
    146     result.first = _insert(std::forward<P>(p));
    147     result.second = result.first ? true : false;
    148     return result;
    149   }
    150 
    151   template<class... Args>
    152   std::pair<iterator, bool> emplace(Args&&... args)
    153   {
    154     std::pair<iterator, bool> result(nullptr, false);
    155     value_type _v(std::forward<Args>(args)...);
    156     result.first = _insert(std::move(_v));
    157     result.second = result.first ? true : false;
    158     return result;
    159   }
    160 
    161   mapped_type& operator[](const key_type& key)
    162   {
    163     value_type *pair_ = find(key);
    164     if(!pair_)
    165     {
    166       pair_ = insert(std::make_pair(key,mapped_type()));
    167     }
    168     return pair_->second;
    169   }
    170 
    171   mapped_type& operator[](key_type&& key)
    172   {
    173     value_type *pair_ = find(key);
    174     if(!pair_)
    175     {
    176       pair_ = insert(std::make_pair(std::move(key), std::move(mapped_type())));
    177     }
    178     return pair_->second;
    179   }
    180 
    181   void erase(const key_type& key)
    182   {
    183     assert(empty_key_ != deleted_key_ && "you must set a deleted key value before delete it");
    184     value_type *pair = find(key);
    185     if(pair && equaler_(key,pair->first))
    186       set_key_deleted(pair);
    187     --size_;
    188     decrease_capacity();
    189   }
    190   void erase(const value_type* value)
    191   {
    192     if(value) erase(value->first);
    193   }
    194   void clear()
    195   {
    196     if(empty()) return;
    197     for(size_t idx = 0; idx < capacity_; ++idx)
    198     {
    199       buckets_[idx]->first = empty_key_;
    200       buckets_[idx]->second = mapped_type();
    201     }
    202     size_ = 0;
    203   }
    204   //bool (const value_type&);
    205   template<class Fn>
    206   void for_each(Fn f) const
    207   {
    208     if(empty()) return;
    209     for(size_t idx = 0; idx < capacity_; ++idx)
    210     {
    211       if(is_key_deleted(buckets_[idx].first) ||
    212          is_key_empty(buckets_[idx].first))
    213         continue;
    214       if(!f(buckets_[idx]))
    215         break;
    216     }
    217   }
    218 
    219   inline void set_deleted_key(key_type k)
    220   {
    221     assert(empty_key_ != k);
    222     if(deleted_key_ != empty_key_)
    223       assert(deleted_key_ == k);
    224     deleted_key_ = k;
    225   }
    226   inline bool empty() const { return size_ == 0; }
    227   inline size_type size() const { return size_; }
    228   inline size_type capacity() const { return capacity_; }
    229  private:
    230   //return key equal position
    231   //or first deleted postion
    232   //or empty postion
    233   value_type* find_position(const key_type& key) const
    234   {
    235     size_type hash_pair_ = hasher_(key);
    236     size_type mask_ = capacity_ - 1;
    237     size_type begin_ = hash_pair_ & mask_;
    238     size_type times_ = 0;
    239     value_type *first_deleted_ = NULL;
    240     while(true)
    241     {
    242       if(is_key_deleted(buckets_[begin_].first) && !first_deleted_)
    243         first_deleted_ = &buckets_[begin_];
    244       else if(is_key_empty(buckets_[begin_].first))
    245       {
    246         if(first_deleted_) return first_deleted_;
    247         return &buckets_[begin_];
    248       }
    249       else if(equaler_(key,buckets_[begin_].first))
    250         return &buckets_[begin_];
    251 
    252       begin_ = (begin_ + 1) & mask_;
    253       assert(times_++ <= capacity_);
    254       (void)times_;
    255     }
    256     return NULL;
    257   }
    258   void copy_from(hash_map&& m)
    259   {
    260     if(m.empty()) return;
    261     for(size_t idx = 0; idx < m.capacity_; ++idx)
    262     {
    263       if(is_key_deleted(m.buckets_[idx].first) ||
    264          is_key_empty(m.buckets_[idx].first))
    265         continue;
    266       _insert(std::move(m.buckets_[idx]));
    267     }
    268   }
    269   void copy_from(const hash_map& m)
    270   {
    271     if(m.empty()) return;
    272     for(size_t idx = 0; idx < m.capacity_; ++idx)
    273     {
    274       if(is_key_deleted(m.buckets_[idx].first) ||
    275          is_key_empty(m.buckets_[idx].first))
    276         continue;
    277       _insert(m.buckets_[idx]);
    278     }
    279   }
    280   void increase_capacity()
    281   {
    282     if(size_ > (capacity_ >> 1))
    283     {
    284       hash_map _m(std::move(*this),capacity_ << 1);
    285       swap(_m);
    286     }
    287   }
    288   void decrease_capacity()
    289   {
    290     if(size_ < (capacity_ >> 2))
    291     {
    292       hash_map _m(*this,capacity_ >> 2);
    293       swap(_m);
    294     }
    295   }
    296   void set_key_deleted(value_type& pair)
    297   {
    298       pair.first = deleted_key_;
    299       pair.second = mapped_type();
    300   }
    301   inline bool is_key_deleted(const key_type& key) const { return equaler_(key,deleted_key_); }
    302   inline bool is_key_empty(const key_type& key) const { return equaler_(key,empty_key_); }
    303   void init_buckets()
    304   {
    305     delete[] buckets_;
    306     buckets_ = new value_type[capacity_]();
    307     if(empty_key_ != key_type())
    308     {
    309       for(unsigned idx = 0; idx < capacity_; ++idx)
    310       {
    311         const_cast<key_type&>(buckets_[idx].first) = empty_key_;
    312       }
    313     }
    314   }
    315   void delete_buckets()
    316   {
    317     delete[] buckets_;
    318   }
    319   value_type* _insert(const value_type& _v)
    320   {
    321     const key_type& key = _v.first;
    322     if(is_key_deleted(key) || is_key_empty(key))
    323       return NULL;
    324     increase_capacity();
    325     value_type *pair_ = find_position(key);
    326     if(!pair_ || equaler_(key,pair_->first))
    327       return NULL;
    328 
    329     auto& k1 = const_cast<key_type&>(pair_->first);
    330     auto& v1 = const_cast<mapped_type&>(pair_->second);
    331     k1 = key;
    332     v1 = _v.second;
    333 
    334     ++size_;
    335     return pair_;
    336   }
    337   template<class P>
    338   value_type* _insert(P&& p)
    339   {
    340     std::pair<key_type, mapped_type> _v(p.first, p.second);
    341     const key_type& key = _v.first;
    342     if(is_key_deleted(key) || is_key_empty(key))
    343       return NULL;
    344     increase_capacity();
    345     value_type *pair_ = find_position(key);
    346     if(!pair_ || equaler_(key,pair_->first))
    347       return NULL;
    348 
    349     auto& k1 = const_cast<key_type&>(pair_->first);
    350     auto& v1 = const_cast<mapped_type&>(pair_->second);
    351     k1 = std::move(_v.first);
    352     v1 = std::move(_v.second);
    353 
    354     ++size_;
    355     return pair_;
    356   }
    357  private:
    358   key_type empty_key_;
    359   key_type deleted_key_;
    360   size_type size_;
    361   size_type capacity_;
    362   value_type *buckets_;
    363   hash_fn hasher_;
    364   equal_fn equaler_;
    365 };
    366 
    367 }//end namespace green_turtle
    368 #endif//__MY_HASH_TABLE__

    参考:

    1. 算法导论

    2. 计算机程序设计艺术

    3. google-sparsehash dense_hash_map的实现, http://code.google.com/p/google-sparsehash

    PS:

    如果有一个好的内存分配器,STL的开链法hash table性能并不差太多,所以我砍掉了自己实现的hash table,代码贴在上面.加入了C++11的move语义,可能会有一些bug,move实在是太繁琐了.

  • 相关阅读:
    HashMap原理
    高并发架构系列:MQ消息队列的12点核心原理总结
    大话程序员系列:一张图道尽程序员的出路
    java面试题
    SpringBoot框架的使用
    java开发定时任务执行时间
    OpenLayers 3 扩展插件收集
    Vue-cli webpack模板
    Spring的属性文件properties使用注意
    FullBg-网页图片背景自适应大小
  • 原文地址:https://www.cnblogs.com/egmkang/p/2325474.html
Copyright © 2020-2023  润新知