[C++]怎么样实现一个较快的Hash Table
2012-01-18 14:40
330 查看
我们服务器一直在用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. 算法导论
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实在是太繁琐了.
当我自己尝试着实现了一个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,否则会复制很多次.
代码贴上:
//Copyright 2012, egmkang wang. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of green_turtle nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // author: egmkang (egmkang@gmail.com) #ifndef __MY_HASH_TABLE__ #define __MY_HASH_TABLE__ #include <utility> #include <functional> #include <cstddef> #include <stdlib.h> namespace green_turtle{ //hash_table with linear probing template<class Key, class T, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key> > class hash_map { public: typedef Key key_type; typedef T mapped_type; typedef std::pair<const Key,T> value_type; typedef size_t size_type; typedef Hash hash_fn; typedef KeyEqual equal_fn; typedef value_type* iterator; hash_map(size_type capacity = 32,key_type empty = key_type(),key_type deleted = key_type()): empty_key_(empty) ,deleted_key_(deleted) ,size_(0) ,capacity_(capacity) ,buckets_(nullptr) ,hasher_() ,equaler_() { init_buckets(); } ~hash_map() { delete_buckets(); } hash_map(hash_map&& m,size_type capacity = 32): buckets_(nullptr) { empty_key_ = m.empty_key_; deleted_key_ = m.deleted_key_; size_ = 0; capacity_ = m.capacity_; //to impl the increase and decrease method if(capacity_ != capacity && capacity >= 32) capacity_ = capacity; hasher_ = m.hasher_; equaler_ = m.equaler_; init_buckets(); copy_from(m); } hash_map& operator = (const hash_map& m) { empty_key_ = m.empty_key_; deleted_key_ = m.deleted_key_; size_ = 0; capacity_ = m.capacity_; hasher_ = m.hasher_; equaler_ = m.equaler_; init_buckets(); copy_from(m); } void swap(hash_map& m) { std::swap(empty_key_ , m.empty_key_); std::swap(deleted_key_ , m.deleted_key_); std::swap(size_ , m.size_); std::swap(capacity_ , m.capacity_); std::swap(hasher_ , m.hasher_); std::swap(equaler_ , m.equaler_); std::swap(buckets_ , m.buckets_); } iterator end() { return nullptr; } iterator end() const { return nullptr; } iterator find(const key_type& key) { if(is_key_empty(key) || is_key_deleted(key)) return NULL; iterator pair_ = find_position(key); if(!pair_ || !equaler_(key,pair_->first)) return NULL; return pair_; } iterator find(const key_type& key) const { if(is_key_empty(key) || is_key_deleted(key)) return NULL; iterator pair_ = find_position(key); if(!pair_ || !equaler_(key,pair_->first)) return NULL; return pair_; } std::pair<iterator, bool> insert(const value_type& v) { std::pair<iterator, bool> result(nullptr, false); result.first = _insert(v); result.second = result.first ? true : false; return result; } template<class P> std::pair<iterator, bool> insert(P&& p) { std::pair<iterator, bool> result(nullptr, false); result.first = _insert(std::forward<P>(p)); result.second = result.first ? true : false; return result; } template<class... Args> std::pair<iterator, bool> emplace(Args&&... args) { std::pair<iterator, bool> result(nullptr, false); value_type _v(std::forward<Args>(args)...); result.first = _insert(std::move(_v)); result.second = result.first ? true : false; return result; } mapped_type& operator[](const key_type& key) { value_type *pair_ = find(key); if(!pair_) { pair_ = insert(std::make_pair(key,mapped_type())); } return pair_->second; } mapped_type& operator[](key_type&& key) { value_type *pair_ = find(key); if(!pair_) { pair_ = insert(std::make_pair(std::move(key), std::move(mapped_type()))); } return pair_->second; } void erase(const key_type& key) { assert(empty_key_ != deleted_key_ && "you must set a deleted key value before delete it"); value_type *pair = find(key); if(pair && equaler_(key,pair->first)) set_key_deleted(pair); --size_; decrease_capacity(); } void erase(const value_type* value) { if(value) erase(value->first); } void clear() { if(empty()) return; for(size_t idx = 0; idx < capacity_; ++idx) { buckets_[idx]->first = empty_key_; buckets_[idx]->second = mapped_type(); } size_ = 0; } //bool (const value_type&); template<class Fn> void for_each(Fn f) const { if(empty()) return; for(size_t idx = 0; idx < capacity_; ++idx) { if(is_key_deleted(buckets_[idx].first) || is_key_empty(buckets_[idx].first)) continue; if(!f(buckets_[idx])) break; } } inline void set_deleted_key(key_type k) { assert(empty_key_ != k); if(deleted_key_ != empty_key_) assert(deleted_key_ == k); deleted_key_ = k; } inline bool empty() const { return size_ == 0; } inline size_type size() const { return size_; } inline size_type capacity() const { return capacity_; } private: //return key equal position //or first deleted postion //or empty postion value_type* find_position(const key_type& key) const { size_type hash_pair_ = hasher_(key); size_type mask_ = capacity_ - 1; size_type begin_ = hash_pair_ & mask_; size_type times_ = 0; value_type *first_deleted_ = NULL; while(true) { if(is_key_deleted(buckets_[begin_].first) && !first_deleted_) first_deleted_ = &buckets_[begin_]; else if(is_key_empty(buckets_[begin_].first)) { if(first_deleted_) return first_deleted_; return &buckets_[begin_]; } else if(equaler_(key,buckets_[begin_].first)) return &buckets_[begin_]; begin_ = (begin_ + 1) & mask_; assert(times_++ <= capacity_); (void)times_; } return NULL; } void copy_from(hash_map&& m) { if(m.empty()) return; for(size_t idx = 0; idx < m.capacity_; ++idx) { if(is_key_deleted(m.buckets_[idx].first) || is_key_empty(m.buckets_[idx].first)) continue; _insert(std::move(m.buckets_[idx])); } } void copy_from(const hash_map& m) { if(m.empty()) return; for(size_t idx = 0; idx < m.capacity_; ++idx) { if(is_key_deleted(m.buckets_[idx].first) || is_key_empty(m.buckets_[idx].first)) continue; _insert(m.buckets_[idx]); } } void increase_capacity() { if(size_ > (capacity_ >> 1)) { hash_map _m(std::move(*this),capacity_ << 1); swap(_m); } } void decrease_capacity() { if(size_ < (capacity_ >> 2)) { hash_map _m(*this,capacity_ >> 2); swap(_m); } } void set_key_deleted(value_type& pair) { pair.first = deleted_key_; pair.second = mapped_type(); } inline bool is_key_deleted(const key_type& key) const { return equaler_(key,deleted_key_); } inline bool is_key_empty(const key_type& key) const { return equaler_(key,empty_key_); } void init_buckets() { delete[] buckets_; buckets_ = new value_type[capacity_](); if(empty_key_ != key_type()) { for(unsigned idx = 0; idx < capacity_; ++idx) { const_cast<key_type&>(buckets_[idx].first) = empty_key_; } } } void delete_buckets() { delete[] buckets_; } value_type* _insert(const value_type& _v) { const key_type& key = _v.first; if(is_key_deleted(key) || is_key_empty(key)) return NULL; increase_capacity(); value_type *pair_ = find_position(key); if(!pair_ || equaler_(key,pair_->first)) return NULL; auto& k1 = const_cast<key_type&>(pair_->first); auto& v1 = const_cast<mapped_type&>(pair_->second); k1 = key; v1 = _v.second; ++size_; return pair_; } template<class P> value_type* _insert(P&& p) { std::pair<key_type, mapped_type> _v(p.first, p.second); const key_type& key = _v.first; if(is_key_deleted(key) || is_key_empty(key)) return NULL; increase_capacity(); value_type *pair_ = find_position(key); if(!pair_ || equaler_(key,pair_->first)) return NULL; auto& k1 = const_cast<key_type&>(pair_->first); auto& v1 = const_cast<mapped_type&>(pair_->second); k1 = std::move(_v.first); v1 = std::move(_v.second); ++size_; return pair_; } private: key_type empty_key_; key_type deleted_key_; size_type size_; size_type capacity_; value_type *buckets_; hash_fn hasher_; equal_fn equaler_; }; }//end namespace green_turtle #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实在是太繁琐了.
相关文章推荐
- 实现一个简单的c++ list容器(含sort排序 链表归并算法实现)
- 【C++】模拟实现一个复数类,要求实现 加,减,乘,除等基本运算符的重载
- C++实现一个简单的异常日志记录类
- 【C系列】一个C++的粒子群(PSO)算法实现
- 自己实现一个C++ 智能指针
- MUILIB-一个可以实现界面自由配置的C++高级界面库
- 用C++实现一个简单的通讯录
- 用C++实现一个小小的爬虫
- Leetcode——146. LRU Cache 一个优雅的LRU的C++实现
- 用C++实现一个Log系统
- 一个无聊男人的疯狂《数据结构与算法分析-C++描述》学习笔记 用C++/lua/python/bash的四重实现(6)高效率的幂运算
- 用C++实现一个哈希桶(插入,删除,寻找)
- C++正则表达式使用实例--实现一个markdown代码标记转换工具
- C++:用两个栈来实现一个队列,完成队列的Push和Pop操作
- 【转】一个比较实用的VS C++版本号自增的实现方式
- 一个无聊男人的疯狂《数据结构与算法分析-C++描述》学习笔记 用C++/lua/python/bash的四重实现(7)习题2.8 随机数组的三种生成算法
- 一个C++的BlockingQueue实现
- C++模板来实现一个通用的内存池.
- 采用C++的ACE库实现的一个通用的C/S架构通信程序(最终版)
- 两个栈来实现一个队列的C++代码(某公司社会早笔试题)