• C++


    转自:http://zrj.me/archives/1248,转载请注明.(分析得不错)

     

    主要尝试回答下面几个问题:

    1. 一般情况下,使用 hash 结构,需要有桶的概念,那么 unordered_map 是如何自动管理桶的,这个问题其实再细分的话是这样的:
      1. 初始的桶是如何设置的
      2. 当需要扩容的时候,是如何重新分布的
    2. 对于 string,unordered_map 的默认哈希函数是怎样的

    代码位于 /usr/include/c++/4.1.2/tr1/,编译器版本比较老,在这个目录下,有这些文件

    total 308K
    -rw-r--r-- 1 root root 3.2K 2007-05-03 20:55 utility
    -rw-r--r-- 1 root root 5.5K 2007-05-03 20:55 unordered_set
    -rw-r--r-- 1 root root 5.8K 2007-05-03 20:55 unordered_map
    -rw-r--r-- 1 root root 5.2K 2007-05-03 20:55 type_traits_fwd.h
    -rw-r--r-- 1 root root  20K 2007-05-03 20:55 type_traits
    -rw-r--r-- 1 root root 4.8K 2007-05-03 20:55 tuple_iterate.h
    -rw-r--r-- 1 root root  11K 2007-05-03 20:55 tuple
    -rw-r--r-- 1 root root  41K 2007-05-03 20:55 repeat.h
    -rw-r--r-- 1 root root 1.9K 2007-05-03 20:55 ref_wrap_iterate.h
    -rw-r--r-- 1 root root 2.0K 2007-05-03 20:55 ref_fwd.h
    -rw-r--r-- 1 root root 2.3K 2007-05-03 20:55 mu_iterate.h
    -rw-r--r-- 1 root root 2.0K 2007-05-03 20:55 memory
    -rw-r--r-- 1 root root  63K 2007-05-03 20:55 hashtable
    -rw-r--r-- 1 root root  28K 2007-05-03 20:55 functional_iterate.h
    -rw-r--r-- 1 root root  36K 2007-05-03 20:55 functional
    -rw-r--r-- 1 root root  24K 2007-05-03 20:55 boost_shared_ptr.h
    -rw-r--r-- 1 root root 8.1K 2007-05-03 20:55 bind_repeat.h
    -rw-r--r-- 1 root root 2.8K 2007-05-03 20:55 bind_iterate.h
    -rw-r--r-- 1 root root 7.4K 2007-05-03 20:55 array

    需要注意的是,unorder_map 和 unorder_set,其实都是一个封装而已,底下用的是 hashtable,所以分析也着重分析 hashtable

    先来看一个典型的操作,[ ] 运算符,在 679 行附近,有这样的代码

      template<typename K, typename Pair, typename Hashtable>
        typename map_base<K, Pair, extract1st<Pair>, true, Hashtable>::mapped_type&
        map_base<K, Pair, extract1st<Pair>, true, Hashtable>::
        operator[](const K& k)
        {
          Hashtable* h = static_cast<Hashtable*>(this);
          typename Hashtable::hash_code_t code = h->m_hash_code(k);
          std::size_t n = h->bucket_index(k, code, h->bucket_count());
    
          typename Hashtable::node* p = h->m_find_node(h->m_buckets[n], k, code);
          if (!p)
    	return h->m_insert_bucket(std::make_pair(k, mapped_type()),
    				  n, code)->second;
          return (p->m_v).second;
        }
    

    可以看到,这是典型的 hash 操作的写法

    1. 先对 key 算出 hash code
    2. 找到这个 hash code 对应的桶
    3. 在这个桶里面,遍历去找这个 key 对应的节点
    4. 把节点返回

    需要注意的是,如果找不到节点,不是返回空,而是会创建一个新的空白节点,然后返回这个空白节点,这里估计是受到返回值的约束,因为返回值声明了必须为一个引用,所以总得搞一个东西出来才能有的引用

    接下来看初始化过程,gdb 跟踪代码可以发现,在 /usr/include/c++/4.1.2/tr1/unordered_map:86,有下面这样的代码,可以看到,初始化的桶大小,被写死为 10。

          explicit
          unordered_map(size_type n = 10,
    		    const hasher& hf = hasher(),
    		    const key_equal& eql = key_equal(),
    		    const allocator_type& a = allocator_type())
          : Base(n, hf, Internal::mod_range_hashing(),
    	     Internal::default_ranged_hash(),
    	     eql, Internal::extract1st<std::pair<const Key, T> >(), a)
          { }
    

    但是,我们看一下下面这个代码的输出

    #include <tr1/unordered_map>
    #include <string>
    #include <stdio.h>
    
    int main() {
        std::tr1::unordered_map<std::string, int> m;
        printf("%d
    ", m.bucket_count());
        return 0;
    }
    

    输出是 11。为什么呢,这个涉及到 rehash。他是初始化为 10,然后 rehash 为 11 了。

    rehash 有两个问题,一个是判断什么时候需要 rehash,一个是怎么 rehash。

    need_rehash 在 hasttable 的 614 附近:

      inline std::pair<bool, std::size_t>
      prime_rehash_policy::
      need_rehash(std::size_t n_bkt, std::size_t n_elt, std::size_t n_ins) const
      {
        if (n_elt + n_ins > m_next_resize)
          {
    	float min_bkts = (float(n_ins) + float(n_elt)) / m_max_load_factor;
    	if (min_bkts > n_bkt)
    	  {
    	    min_bkts = std::max(min_bkts, m_growth_factor * n_bkt);
    	    const unsigned long* const last = X<>::primes + X<>::n_primes;
    	    const unsigned long* p = std::lower_bound(X<>::primes, last,
    						      min_bkts, lt());
    	    m_next_resize =
    	      static_cast<std::size_t>(std::ceil(*p * m_max_load_factor));
    	    return std::make_pair(true, *p);
    	  }
    	else
    	  {
    	    m_next_resize =
    	      static_cast<std::size_t>(std::ceil(n_bkt * m_max_load_factor));
    	    return std::make_pair(false, 0);
    	  }
          }
        else
          return std::make_pair(false, 0);
      }
    

    来看他是怎么做的,首先是用一个 m_max_load_factor 的因子来判断目前的容量需要多少个哈希桶,如果需要 rehash,那么使用素数表来算出新的桶需要多大。

    素数表在 491 行附近:

    template<int ulongsize>
        const unsigned long X<ulongsize>::primes[256 + 48 + 1] =
        {
          2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,
    

    初始的时候,m_max_load_factor(1), m_growth_factor(2), m_next_resize(0),根据 std::lower_bound 来找到比 10 大的最小素数是 11,于是就分配为 11 个桶。

    rehash 就很平淡无奇了,一个一个重算,然后重新填进去,没有什么特别的。

      template<typename K, typename V,
    	   typename A, typename Ex, typename Eq,
    	   typename H1, typename H2, typename H, typename RP,
    	   bool c, bool ci, bool u>
        void
        hashtable<K, V, A, Ex, Eq, H1, H2, H, RP, c, ci, u>::
        m_rehash(size_type n)
        {
          node** new_array = m_allocate_buckets(n);
          try
    	{
    	  for (size_type i = 0; i < m_bucket_count; ++i)
    	    while (node* p = m_buckets[i])
    	      {
    		size_type new_index = this->bucket_index(p, n);
    		m_buckets[i] = p->m_next;
    		p->m_next = new_array[new_index];
    		new_array[new_index] = p;
    	      }
    	  m_deallocate_buckets(m_buckets, m_bucket_count);
    	  m_bucket_count = n;
    	  m_buckets = new_array;
    	}
          catch(...)
    	{
    	  // A failure here means that a hash function threw an exception.
    	  // We can't restore the previous state without calling the hash
    	  // function again, so the only sensible recovery is to delete
    	  // everything.
    	  m_deallocate_nodes(new_array, n);
    	  m_deallocate_buckets(new_array, n);
    	  m_deallocate_nodes(m_buckets, m_bucket_count);
    	  m_element_count = 0;
    	  __throw_exception_again;
    	}
        }
    

    然后就是 hash 函数了。hash 函数位于 /usr/include/c++/4.1.2/tr1/functional:1194,对于 std::string,用的是下面这种 hash 函数

      template<>
        struct Fnv_hash<8>
        {
          static std::size_t
          hash(const char* first, std::size_t length)
          {
    	std::size_t result = static_cast<std::size_t>(14695981039346656037ULL);
    	for (; length > 0; --length)
    	  {
    	    result ^= (std::size_t)*first++;
    	    result *= 1099511628211ULL;
    	  }
    	return result;
          }
        };
    

    这个叫 FNV hash,http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function,FNV 有分版本,例如 FNV-1 和 FNV-1a,区别其实就是先异或再乘,或者先乘在异或,这里用的是 FNV-1a,为什么呢,维基里面说,The small change in order leads to much better avalanche characteristics,什么叫 avalanche characteristics 呢,这个是个密码学术语,叫雪崩效应,意思是说输入的一个非常微小的改动,也会使最终的 hash 结果发生非常巨大的变化,这样的哈希效果被认为是更好的。

     
  • 相关阅读:
    STM32.ADC
    电源方案集
    什么叫二级域名
    android驱动学习---led实验
    Get,Post和Head具体解释
    Android 编码规范
    VC:当前不会命中断点,还没有为该文档载入不论什么符号
    经常使用的结构体
    【Facebook的UI开发框架React入门之九】button简单介绍(iOS平台)-goodmao
    记录遇到的ios下的bugs[废弃]
  • 原文地址:https://www.cnblogs.com/crazyacking/p/5281128.html
Copyright © 2020-2023  润新知