STLport::hash_map实现分析


前言:

       最近研究了下STLport::hash_map的底层实现。由于STLport是基于SGI实现的,所以也可以通过STLport大致一窥SGI的内部逻辑。

STLport下载地址如下:

http://nchc.dl.sourceforge.net/project/stlport/STLport/STLport-5.2.1/STLport-5.2.1.tar.bz2


叙个大概,几个层次

       以最简单的插入数据来讲,从顶向下,主要stlp_std::hash_map,stlp_std::hashtable和stlp_std::slist三层:

stlp_std::hash_map包装了底层实现,向上层提供服务,stlp_std::hashtable提供对任何有名项的存取操作和删除操作,stlp_std::slist负责对最底层的数据的保存和链表的操作。首先准备一个简单的stlp_std::hash_map应用:

#include <iostream>
#include <hash_map>

typedef stlp_std::hash_map<char , std::string> HASHMAP;
typedef HASHMAP::iterator HASHMAP_ITER;


int main(int argc ,char *argv[])
{

	HASHMAP hsmp;
	hsmp['a'] = "yoshino";
	hsmp['b'] = "akaki";
	hsmp['b'] = "namiwa";
	hsmp['c'] = "akitsushima";
	hsmp['d'] = "iwashima";
	hsmp['e'] = "hashitachi";
	hsmp['f'] = "matsushima";
	
	return 0;
}

这Code没什么特别的,就是向hsmp添加键值。hsmp也是简单至极,只有<key , Value>


stlp_std::hash_map:

我们在声明hsmp时没有指定hash函数和比较函数,所以stlp_std::hash_map的构造中会默认指定(注意,这里我的键是普通类型char,如果是复合类型如结构或是类的话,就必须自定义指定hash函数和比较函数):

hash_map() : _M_ht(0, hasher(), key_equal(), allocator_type()) {}
这里指定的hash函数hasher()和键比较函数key_equal(),都是仿函数,分别定义在:
//_hash_fun.h
_STLP_TEMPLATE_NULL struct hash<char> {
	size_t operator()(char __x) const { return __x; }//返回char的ASCII
};
//_function_base.h
struct equal_to : public binary_function<_Tp, _Tp, bool> {
	bool operator()(const _Tp& __x, const _Tp& __y) const { return __x == __y; }
};
在_hash_fun.h中,定义了很多普通类型的hash函数,这里的struct hash<char>就是其中之一,它仅仅是将char值以ASCII值返回。如果你要使用复合类型,就要自己定义hash函数拉,这也是hash_map麻烦的地方。

程序执行添加键值是调用了stlp_std::hash_map的[]重载:

_STLP_TEMPLATE_FOR_CONT_EXT
  _Tp& operator[](const _KT& __key) {
    iterator __it = _M_ht.find(__key);  //搜索键

    return (__it == _M_ht.end() ?  //如果没有搜索到键就执行插入,如果键已存在则返回值
      _M_ht._M_insert(value_type(__key, _STLP_DEFAULT_CONSTRUCTED(_Tp))).second : //hashtable _M_ht;
      (*__it).second );
  }

stlp_std::hash_map中定义了stlp_std::hashtable_M_ht,由它实际处理。在stlp_std::hash_map的[]重载中,首先在_M_ht中搜索键是否存在,如果不存在,则由_M_ht新建插入,如果存在,则返回值。我们新插入键值对<'a',"yoshino">。所以执行hashtable<...>::_M_insert(const value_type& __obj)。程序由此进入stlp_std::hashtable层。


stlp_std::hashtable

hash table表格内的元素称之为(bucket),侯捷先森《STL源码剖析》中介绍了开链法实现的hashtable,如图

而在STLport中,hashtable的实现如下:


其中_BucketVector中的每个元素保存有链表_Slist_node相应的节点的指针。在stlp_std::hashtable定义了bucket:

//typedef slist<value_type, _All> _ElemsCont;
_ElemsCont            _M_elems;	 //bucket list
//typedef _STLP_PRIV _Slist_node_base _BucketType;
//typedef vector<_BucketType*, _BucketAllocType> _BucketVector;  
_BucketVector         _M_buckets;  //存储桶
size_type             _M_num_elements; //标示hashtable元素个数,size()函数返回的即是此

其中_M_buckets就是存储桶,他是个vector数组,每个元素又是_Slist_node_base链表(图中的bucket list)。其中_Slist_node,_Slist_node_base等数据结构将在后面的stlp_std::slist介绍。之前在stlp_std::hash_map程序走到了_M_insert(const value_type& __obj),其实现如下:

template <class _Val, class _Key, class _HF,
          class _Traits, class _ExK, class _EqK, class _All>
__reference__
hashtable<_Val,_Key,_HF,_Traits,_ExK,_EqK,_All>
  ::_M_insert(const value_type& __obj) {
  _M_enlarge(_M_num_elements + 1);  //检查最大负载因子时的桶容量,如果这个容量大于当前桶容量,则需要扩展桶容量
  return *insert_unique_noresize(__obj).first; //在不需要重建表格的情况下插入新节点。键值不允许重复
}
这里涉及到hashmap中的一个概念——负载因子。负载因子=当前数据量/桶的总容量,实际就是占用空间的百分比。这里的_M_enlarge负责检查是否需要扩展桶容量,其实现如下:
template <class _Val, class _Key, class _HF,
          class _Traits, class _ExK, class _EqK, class _All>
void hashtable<_Val,_Key,_HF,_Traits,_ExK,_EqK,_All>
::_M_enlarge(size_type __to_size){ // __to_size = _M_num_elements + 1
  size_type __num_buckets = bucket_count(); //桶个数
  size_type __num_buckets_hint = (size_type)((float)__to_size / max_load_factor()); //散列表中节点的数目除以基本区域能容纳的节点数
  //max_load_factor()获取最大负载因子 , __num_buckets_hint代表获取最大负载因子时的HashMap的总容量,此时__num_buckets_hint处在最小状态
  if (__num_buckets_hint <= __num_buckets) {
    return;   //如果最大负载因子的HashMap的总容量小于当前容量,则什么也不做
  }
  __num_buckets = _STLP_PRIV _Stl_prime_type::_S_next_size(__num_buckets_hint);//返回桶大小下至要求尺寸

  _M_rehash(__num_buckets);  //无条件地改变桶容量为__num_buckets
随后,程序执行hashtable<...>::insert_unique_noresize(const value_type& __obj):
template <class _Val, class _Key, class _HF,
          class _Traits, class _ExK, class _EqK, class _All>
pair<__iterator__, bool>
hashtable<_Val,_Key,_HF,_Traits,_ExK,_EqK,_All>
  ::insert_unique_noresize(const value_type& __obj) {
  const size_type __n = _M_bkt_num(__obj); //获取捅号
  _ElemsIte __cur(_M_buckets[__n]);
  _ElemsIte __last(_M_buckets[__n + 1]);

  if (__cur != __last) {
    for (; __cur != __last; ++__cur) {
      if (_M_equals(_M_get_key(*__cur), _M_get_key(__obj))) {
        //We check that equivalent keys have equals hash code as otherwise, on resize,
        //equivalent value might not be in the same bucket
        _STLP_ASSERT(_M_hash(_M_get_key(*__cur)) == _M_hash(_M_get_key(__obj)))
        return pair<iterator, bool>(iterator(__cur), false);
      }
    }
    /* Here we do not rely on the _M_insert_noresize method as we know
     * that we cannot break element orders, elements are unique, and
     * insertion after the first bucket element is faster than what is
     * done in _M_insert_noresize.
     */
    __cur = _M_elems.insert_after(_ElemsIte(_M_buckets[__n]), __obj);
    ++_M_num_elements;//累加vector线性表元素个数
    return pair<iterator, bool>(iterator(__cur), true);
  }

  return pair<iterator, bool>(_M_insert_noresize(__n, __obj), true);
}

在insert_unique_noresize里,我们首先要计算出一个桶号,作为桶中存储的下标。方法是执行_M_bkt_num , 参数是键值对__obj,_M_bkt_num最终调用到_M_bkt_num_key。执行_M_hash(__key) % __n,求出桶号,这里的_M_hash就是之前在stlp_std::hash_map中默认指定的struct hash<char>。所以,这里的算出的桶号就是我们制定的char键的ASC码和桶容量做求模运算。

size_type _M_bkt_num_key(const _KT& __key) const
{ return _M_bkt_num_key(__key, bucket_count()); }

size_type _M_bkt_num(const value_type& __obj) const
{ return _M_bkt_num_key(_M_get_key(__obj)); }

_STLP_TEMPLATE_FOR_CONT_EXT
size_type _M_bkt_num_key(const _KT& __key, size_type __n) const
{
     return return _M_hash(__key) % __n;   //_M_hash(__key) % bucket_count()求出桶号
}

获取桶号后,程序检查是否有相同hash码的等效key,防止它们处在同一个桶中。随后执行hashtable<...>::_M_insert_noresize(size_type __n, const value_type& __obj):

template <class _Val, class _Key, class _HF,
          class _Traits, class _ExK, class _EqK, class _All>
__iterator__
hashtable<_Val,_Key,_HF,_Traits,_ExK,_EqK,_All>
  ::_M_insert_noresize(size_type __n, const value_type& __obj) {   //__n代表算出的捅号 , __obj是键值对
  //We always insert this element as 1st in the bucket to not break
  //the elements order as equal elements must be kept next to each other.
  size_type __prev = __n; //桶号
  _ElemsIte __pos = _M_before_begin(__prev)._M_ite;                  //_pos根据桶号__prev获取插入点iter
  //填充数据节点到_M_buckets.begin() + __prev
  fill(_M_buckets.begin() + __prev, _M_buckets.begin() + __n + 1,
      _M_elems.insert_after(__pos, __obj)._M_node);   //_M_elems是slist,在__pos的下一个位置插入新节点,值为__obj,这里的最终是新节点,数据最终存储到slist::_M_head _Slist_node_base _M_node
  ++_M_num_elements; //累加捅数目
  return iterator(_ElemsIte(_M_buckets[__n]));                              //返回新捅号数据的迭代
}

这里传给_M_insert_noresize两个参数,__n代表算出的捅号 , __obj是键值对。然后由__pos根据桶号获取下一个插入位置,_M_elems.insert_after将键值对__obj插入到位置__pos之后。fill填充将链表头填充到_M_buckets.begin() + __prev,也就是桶号位置。我们在程序中首先插入['a',"..."],获取桶号6,则在_BucketVector::_M_start接上['a',"..."](插入到链表_Slist_node,具体介绍后面介绍),当插入['b',"..."]时,获取桶号0,则将['b',"..."]插入['a',"..."]之前,作为_BucketVector::_M_start中的第一个元素。随后再插入['c',"..."],获取桶号2,则将['c',"..."]插入['b',"..."]之后。以此类推,如下图:



stlp_std::slist:

hashtable将数据存储存储在_M_elems,它是个slist类型。在这层,主要根据上层的指令,代理完成链表的相关操作,下面介绍几个相关的数据类型:

//单向链表的节点基本结构
struct _Slist_node_base {
	_Slist_node_base* _M_next;
};

//单向链表的节点结构
template <class _Tp>
class _Slist_node : public _Slist_node_base {
public:
        _Tp _M_data;              //键值对最后存储在这里
        __TRIVIAL_STUFF(_Slist_node);
};

template <class _Tp, class _Alloc>
class _Slist_base {
        //...
        typedef _STLP_alloc_proxy<_Slist_node_base, _Node, _M_node_allocator_type> _AllocProxy;
        //...
        _AllocProxy _M_head;
};


template <class _Tp, class _Alloc>
class slist : protected _STLP_PRIV _Slist_base<_Tp,_Alloc>
{
        //...
};
首先可见 _Slist_node_base和继承它的_Slist_node ,键值对 数据就最终存储在 链表结构_Slist_node:: _M_data ,各个键值对之间通过 _Slist_node_base :: _M_next 关联

由_M_elems.insert_after(__pos, __obj)进入,它最终执行:

__STATIC_CAST(_Node*, _STLP_PRIV __slist_make_link(__pos, _M_create_node(__x)));
这里我们调用 _M_create_node(__x)建立一个新的_Slist_node实例(_Slist_node_base派生) ,然后执行 __slist_make_link 将这个新实例插入到 __pos之后, _M_create_node 实现如下:

_Node* _M_create_node(const value_type& __x) {
   //typedef _STLP_PRIV _Slist_node<_Tp> _Node;
    _Node* __node = this->_M_head.allocate(1);      //新建一个node,并分配空间
    _STLP_TRY {
      _Copy_Construct(&__node->_M_data, __x);        //配置新node的数据
      __node->_M_next = 0;                                         //单向链表,end()->0;
    }
    _STLP_UNWIND(this->_M_head.deallocate(__node, 1))
    return __node;                                                        //返回新加的node
  }
这样,新的数据就被建立在 _node中 ,然后_node被插入到__pos之后,这样链表的操作就完成了。回到 stlp_std :: hashtable的hashtable<...>::_M_insert_noresize
fill(_M_buckets.begin() + __prev, _M_buckets.begin() + __n + 1,
      _M_elems.insert_after(__pos, __obj)._M_node);   //_M_elems是slist,在__pos的下一个位置插入新节点,值为__obj,这里的最终是新节点,数据最终存储到slist::_M_head _Slist_node_base _M_node
  ++_M_num_elements; //累加捅数目
  return iterator(_ElemsIte(_M_buckets[__n])); 
_M_elems.insert_after 完成了底层的链表操作, _M_node 返回 _Slist_node_base,由fill将它插入到 _M_buckets.begin() + __prev( __prev 桶号位置 ,然后层层返回指向新加数据的迭代器。这样就完成了stlp_std::hash_map的一次数据添加。


 

                
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值