C++ - unordered_map 源码解析

简介:   转自:http://zrj.me/archives/1248,转载请注明.(分析得不错)   主要尝试回答下面几个问题: 一般情况下,使用 hash 结构,需要有桶的概念,那么 unordered_map 是如何自动管理桶的,这个问题其实再细分的话是这样的:初始的桶是如何...

 

 

转自: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\n", 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 结果发生非常巨大的变化,这样的哈希效果被认为是更好的。

 
目录
相关文章
|
5月前
|
C++
基本二叉树与排序二叉树(C++源码)
本程序实现二叉树基本操作与二叉排序树应用。支持前序建树、四种遍历、求深度、叶子数、第K层节点数及查找功能;并实现二叉排序树的构建、中序输出与查找比较次数统计,分析不同插入顺序对树形态和查找效率的影响。
|
12月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
326 2
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
12月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
636 73
|
11月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
222 4
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
9月前
|
安全 Java 数据库连接
让我们讲解一下 Map 集合遍历的方式
我是小假 期待与你的下一次相遇 ~
331 43
|
12月前
使用 entrySet 遍历 Map 类集合 KV
使用 entrySet 遍历 Map 类集合 KV
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用

推荐镜像

更多
  • DNS