C++ - unordered_map 源码解析
转自:http://zrj.me/archives/1248,转载请注明.(分析得不错)
主要尝试回答下面几个问题:
- 一般情况下,使用 hash 结构,需要有桶的概念,那么 unordered_map 是如何自动管理桶的,这个问题其实再细分的话是这样的:
- 初始的桶是如何设置的
- 当需要扩容的时候,是如何重新分布的
- 对于 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 操作的写法
- 先对 key 算出 hash code
- 找到这个 hash code 对应的桶
- 在这个桶里面,遍历去找这个 key 对应的节点
- 把节点返回
需要注意的是,如果找不到节点,不是返回空,而是会创建一个新的空白节点,然后返回这个空白节点,这里估计是受到返回值的约束,因为返回值声明了必须为一个引用,所以总得搞一个东西出来才能有的引用
接下来看初始化过程,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 结果发生非常巨大的变化,这样的哈希效果被认为是更好的。
C++ - unordered_map 源码解析的更多相关文章
- [源码解析] 机器学习参数服务器ps-lite (1) ----- PostOffice
[源码解析] 机器学习参数服务器ps-lite 之(1) ----- PostOffice 目录 [源码解析] 机器学习参数服务器ps-lite 之(1) ----- PostOffice 0x00 ...
- [源码解析] 机器学习参数服务器ps-lite(2) ----- 通信模块Van
[源码解析] 机器学习参数服务器ps-lite(2) ----- 通信模块Van 目录 [源码解析] 机器学习参数服务器ps-lite(2) ----- 通信模块Van 0x00 摘要 0x01 功能 ...
- [源码解析] 机器学习参数服务器ps-lite 之(3) ----- 代理人Customer
[源码解析] 机器学习参数服务器ps-lite 之(3) ----- 代理人Customer 目录 [源码解析] 机器学习参数服务器ps-lite 之(3) ----- 代理人Customer 0x0 ...
- [源码解析]机器学习参数服务器ps-lite(4) ----- 应用节点实现
[源码解析]机器学习参数服务器ps-lite(4) ----- 应用节点实现 目录 [源码解析]机器学习参数服务器ps-lite(4) ----- 应用节点实现 0x00 摘要 0x01 基础类 1. ...
- [源码解析] 机器学习参数服务器 Paracel (1)-----总体架构
[源码解析] 机器学习参数服务器 Paracel (1)-----总体架构 目录 [源码解析] 机器学习参数服务器 Paracel (1)-----总体架构 0x00 摘要 0x01使用 1.1 配置 ...
- [源码解析] 机器学习参数服务器Paracel (3)------数据处理
[源码解析] 机器学习参数服务器Paracel (3)------数据处理 目录 [源码解析] 机器学习参数服务器Paracel (3)------数据处理 0x00 摘要 0x01 切分需要 1.1 ...
- [源码解析] Pytorch 如何实现后向传播 (2)---- 引擎静态结构
[源码解析] Pytorch 如何实现后向传播 (2)---- 引擎静态结构 目录 [源码解析] Pytorch 如何实现后向传播 (2)---- 引擎静态结构 0x00 摘要 0x01 Engine ...
- [源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑
[源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑 目录 [源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑 0x00 摘要 0x01 前文回顾 0 ...
- [源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法
[源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法 目录 [源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法 0x00 摘要 0x01 工作线程主体 1.1 ...
随机推荐
- js实现DOM结构
/* 编写一段js脚本生成下面的DOM结构.要求使用标准的DOM方法或属性 <div id='example'> <p class='slogan'>淘,你喜欢</p&g ...
- [Beautifulzzzz的博客目录] 快速索引点这儿O(∩_∩)O~~,红色标记的是不错的(⊙o⊙)哦~
3D相关开发 [direct-X] 1.direct-X最小框架 [OpenGL] 1.环境搭建及最小系统 [OpenGL] 2.企业版VC6.0自带的Win32-OpenGL工程浅析 51单片机 [ ...
- [译]C++, Java和C#的编译过程解析
1.1.1 摘要 我们知道计算机不能直接理解高级语言,它只能理解机器语言,所以我们必须要把高级语言翻译成机器语言,这样计算机才能执行高级语言编写的程序,在接下来的博文中,我们将介绍非托管和托管语音的编 ...
- ASP.NET Web API 应用教程(一) ——数据流使用
相信已经有很多文章来介绍ASP.Net Web API 技术,本系列文章主要介绍如何使用数据流,HTTPS,以及可扩展的Web API 方面的技术,系列文章主要有三篇内容. 主要内容如下: I 数据 ...
- .NET学习笔记 -- 那堆名词到底是啥(CLR、CLI、CTS、CLS、IL、JIT)
什么是CLR? CLR,公共语言运行时(Common Language Runtime)是一个由多种语言使用的“运行时”.他的核心功能包括(内存管理.程序集加载.安全性.异常处理和线程同步),可以被面 ...
- How to Change RabbitMQ Queue Parameters in Production?
RabbitMQ does not allow re-declaring a queue with different values of parameters such as durability, ...
- Lucene系列-概述
为了生存,就得干一行爱一行.那就学习下lucene吧. 全文检索介绍 流程: 建索引 准备待搜索文档 文档分词:将文档分成一个个单独的单词,去除标点符号.停词(无意义的冠词介词等),得到token 语 ...
- 看svn用户组管理功能的产品设计
我负责公司的svn配置.用了近一年了,今天饶有兴致,分享一下svn的用户组管理功能,这个产品设计值得借鉴,简单易用. svn用户组管理的功能描述:新建用户组,并给组分配成员用户:编辑用户组,包括修改组 ...
- NSDate NSString相互转化
时间戳是经常用到的,今天就总结一下 //设置转化格式 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter s ...
- Redis学习笔记~Redis并发锁机制
回到目录 redis客户端驱动有很多,如ServiceStack.Redis,StackExchange.Redis等等,下面我使用ServiceStack.Redis为例,介绍一下在redis驱动中 ...