Последнее обновление:2026-10-12
Хеш-таблица STL hashtable¶
0. Введение¶
Хеш-таблица — это нижележащий контейнер для unordered_map, unordered_set и других; после gcc2.9 объём исходников сильно вырос!
Читаем по-прежнему код gcc4.9.1; кода очень много, поэтому разворачивать всё не будем — сосредоточимся на искусстве и технике нижележащего хеширования; кажется, эти два слова неплохо рифмуются, ха-ха. Перейдём к сути~
1. Первое знакомство с Hashtable¶
Сначала взглянем на исходники Hashtable:
template<typename _Key, typename _Value, typename _Alloc,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash,
typename _RehashPolicy, typename _Traits>
class _Hashtable
: public __detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal,
public __detail::_Map_base<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Rehash_base<_Key, _Value, _Alloc, _ExtractKey, _Equal,_H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Equality<_Key, _Value, _Alloc, _ExtractKey, _Equal,_H1, _H2, _Hash, _RehashPolicy, _Traits>,
private __detail::_Hashtable_alloc<typename __alloctr_rebind<_Alloc,__detail::_Hash_node<_Value,_Traits::__hash_cached::value> >::__type>
{
};
Кто не учил шаблоны классов, тот впадёт в ступор — только пересчитать параметры шаблона уже головокружение。。。
А ещё его наследование — столько отцов сразу。。。
Ниже по очереди разберём его «отцов», а затем вернёмся к хеш-таблице.
2. _Hashtable_base¶
В комментариях к нему сказано:
Helper class adding management of _Equal functor to _Hash_code_base type.
Вспомогательный класс, добавляющий управление функтором _Equal к типу _Hash_code_base.
Сравнив код, можно понять, что это значит:
template<typename _Key, typename _Value,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash, typename _Traits>
struct _Hashtable_base
: public _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, _Traits::__hash_cached::value>,
private _Hashtable_ebo_helper<0, _Equal>
{
};
_Hash_code_base и _Hashtable_base — они различаются ровно одним _Equal; на этом объяснение этой фразы завершено.
Его базовых классов, в свою очередь, два:
Продолжим отслеживать эти два класса!
2.1 _Hash_code_base¶
Последний параметр этого класса __cache_hash_code показывает, кэшируется ли hash code.
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash,bool __cache_hash_code>
struct _Hash_code_base;
В зависимости от кэширования получаем частично специализированные версии:
- используется ранжированное хеширование (по сути это привычный нам метод деления с остатком), hash code не кэшируется.
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, false>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _Hash>
}
Для этой специализации кэширование не нужно, поэтому в коде есть только объявление, без определения!
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, true>;
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2,
_Default_ranged_hash, false>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _H1>,
private _Hashtable_ebo_helper<2, _H2>
{
};
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2,
_Default_ranged_hash, true>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _H1>,
private _Hashtable_ebo_helper<2, _H2>
{
Что такое _H1 и _H2 выше, наверняка вводит в заблуждение — разберёмся:
(1) Default range hashing function (функция ранжированного хеширования по умолчанию)
Вот это и есть: Подробно об этом можно прочитать далее.struct _Mod_range_hashing
{
typedef std::size_t first_argument_type;
typedef std::size_t second_argument_type;
typedef std::size_t result_type;
result_type
operator()(first_argument_type __num,
second_argument_type __den) const noexcept
{ return __num % __den; }
};
Применительно к структуре данных хеш-таблицы это известная всем хеш-функция: метод деления с остатком.
Далее —_Default_ranged_hash:
Он служит лишь меткой для уже вычисленной по умолчанию функции ранжированного хеширования (Default ranged hash function):
Так что здесь нижележащая хеш-функция хеш-таблицы совершенно очевидна — по умолчанию она именно такая.
А упомянутая метка появилась из-за того, что составление H из объектов типов H1 и H2 потребовало бы лишних операций копирования — отсюда и эта метка.
На этом разбор упомянутых _H1 и _H2 завершён — они соответствуют двум описанным выше функциям.
(2) операция rehash
Следом идёт ещё одна довольно важная вещь — rehash. Думаю, все хорошо понимают rehash: когда конфликтов в хеш-таблице становится определённо много, нужно заново расставить key по подходящим местам; так и устроены нижележащие исходники хеш-таблицы, здесь это инкапсулировано в rehash policy:
В операции rehash сказано: размер корзины (bucket size) по умолчанию обычно является наименьшим простым числом, чтобы коэффициент заполнения (load factor — отношение текущего числа элементов контейнера к числу корзин) оставался достаточно малым. Коэффициент заполнения измеряет, насколько заполнена хеш-таблица; максимальный коэффициент заполнения по умолчанию равен 1.0._Prime_rehash_policy(float __z = 1.0)
: _M_max_load_factor(__z), _M_next_resize(0) { }
> rehash计算下一个素数桶
inline std::size_t
_Prime_rehash_policy::
_M_next_bkt(std::size_t __n) const
{
const unsigned long* __p = std::lower_bound(__prime_list, __prime_list + _S_n_primes, __n);
_M_next_resize =
static_cast<std::size_t>(__builtin_ceil(*__p * _M_max_load_factor));
return *__p;
}
Таблица простых чисел
Как искать простые числа?
Замечаем выше __prime_list — идём искать и находим всю таблицу простых чисел в libstdc++v3/src/shared/hashtable-aux.cc.
Всего в ней 256+1+49 либо 256+49 элементов.
Если sizeof(unsigned long)!=8 — 256+1+49, иначе — 256+49.
extern const unsigned long __prime_list[] = // 256 + 1 or 256 + 48 + 1
{
2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,
37ul, 41ul, 43ul, 47ul, 53ul, 59ul, 61ul, 67ul, 71ul, 73ul, 79ul,
83ul, 89ul, 97ul, 103ul, 109ul, 113ul, 127ul, 137ul, 139ul, 149ul,
// 后面还有很多
}
Вычисление корзины для элемента
По максимальному коэффициенту заполнения вычисляется минимальное число корзин, а затем по числу корзин — минимальная простая корзина для каждого элемента.
inline std::size_t
_Prime_rehash_policy::
_M_bkt_for_elements(std::size_t __n) const
{
// 获取最小的桶
const float __min_bkts = __n / _M_max_load_factor;
// 获取最小素数p
const unsigned long* __p = std::lower_bound(__prime_list, __prime_list
+ _S_n_primes, __min_bkts);
_M_next_resize =
static_cast<std::size_t>(__builtin_ceil(*__p * _M_max_load_factor));
return *__p;
}
_Hashtable_ebo_helper — это уже изученный нами ранее EBO, пустой базовый класс
_Map_base посредством частичной специализации реализует перегрузку операторов [] и at.
`_Insert` отвечает за всё, связанное с вставкой.
`_Rehash_base` отвечает за передачу значения максимального коэффициента заполнения в описанном выше rehash.
`_Equality_base` предоставляет классу _Equality общие типы и функции.
К этому моменту все классы, от которых наследуется _Hashtable, разобраны.
2. Структура узлов связного списка в hashtable¶
Базовый класс hash node — содержит только объявление указателя.
struct _Hash_node_base
{
_Hash_node_base* _M_nxt;
_Hash_node_base() noexcept : _M_nxt() { }
_Hash_node_base(_Hash_node_base* __next) noexcept : _M_nxt(__next) { }
};
template<typename _Value>
struct _Hash_node_value_base : _Hash_node_base
{
typedef _Value value_type;
__gnu_cxx::__aligned_buffer<_Value> _M_storage;
_Value*
_M_valptr() noexcept
{ return _M_storage._M_ptr(); }
const _Value*
_M_valptr() const noexcept
{ return _M_storage._M_ptr(); }
_Value&
_M_v() noexcept
{ return *_M_valptr(); }
const _Value&
_M_v() const noexcept
{ return *_M_valptr(); }
};
Ранее упоминалось, хранит ли узел hash code, — значит, узел должен нести hash code; конкретно это реализовано ниже:
/**
* Primary template struct _Hash_node.
*/
template<typename _Value, bool _Cache_hash_code>
struct _Hash_node;
/**
* Specialization for nodes with caches, struct _Hash_node.
*
* Base class is __detail::_Hash_node_value_base.
*/
template<typename _Value>
struct _Hash_node<_Value, true> : _Hash_node_value_base<_Value>
{
std::size_t _M_hash_code;
_Hash_node*
_M_next() const noexcept
{ return static_cast<_Hash_node*>(this->_M_nxt); }
};
/**
* Specialization for nodes without caches, struct _Hash_node.
*
* Base class is __detail::_Hash_node_value_base.
*/
template<typename _Value>
struct _Hash_node<_Value, false> : _Hash_node_value_base<_Value>
{
_Hash_node*
_M_next() const noexcept
{ return static_cast<_Hash_node*>(this->_M_nxt); }
};
Здесь всё однозначно: узлы делятся на содержащие hash code и не содержащие; конкретная версия частичной специализации выбирается по переданному параметру шаблона.
3. Итератор¶
Базовый класс итератора демонстрирует использование синтаксиса using, похожего на typedef: в последующих определениях можно напрямую использовать запись __node_type; функция _M_incr осуществляет переход к следующему узлу связного списка.
/// Base class for node iterators.
template<typename _Value, bool _Cache_hash_code>
struct _Node_iterator_base
{
using __node_type = _Hash_node<_Value, _Cache_hash_code>;
__node_type* _M_cur;
_Node_iterator_base(__node_type* __p) noexcept
: _M_cur(__p) { }
void
_M_incr() noexcept
{ _M_cur = _M_cur->_M_next(); }
};
Итератор узлов: изучив код ниже, извлечём два урока: - первое: использование using - итератор hashtable принадлежит forward_iterator - перегружены четыре оператора: ++, --, *, ->
template<typename _Value, bool __constant_iterators, bool __cache>
struct _Node_iterator
: public _Node_iterator_base<_Value, __cache>
{
private:
using __base_type = _Node_iterator_base<_Value, __cache>;
using __node_type = typename __base_type::__node_type;
public:
typedef _Value value_type;
typedef std::ptrdiff_t difference_type;
typedef std::forward_iterator_tag iterator_category;
using pointer = typename std::conditional<__constant_iterators,
const _Value*, _Value*>::type;
using reference = typename std::conditional<__constant_iterators,
const _Value&, _Value&>::type;
_Node_iterator() noexcept
: __base_type(0) { }
explicit
_Node_iterator(__node_type* __p) noexcept
: __base_type(__p) { }
reference
operator*() const noexcept
{ return this->_M_cur->_M_v(); }
pointer
operator->() const noexcept
{ return this->_M_cur->_M_valptr(); }
_Node_iterator&
operator++() noexcept
{
this->_M_incr();
return *this;
}
_Node_iterator
operator++(int) noexcept
{
_Node_iterator __tmp(*this);
this->_M_incr();
return __tmp;
}
};
4. Детально изучаем важную внутреннюю структуру hashtable¶
Внутренняя структура такова: в каждом элементе поддерживается односвязный список, а уже на нём выполняются вставка, поиск, удаление и другие операции; каждый элемент называется корзиной (bucket). Нижележащая схема построения: сначала H1 вычисляет hash code для key, затем методом деления с остатком H2 получается соответствующая корзина.
template<typename _Key, typename _Value, typename _Alloc,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash,
typename _RehashPolicy, typename _Traits>
class _Hashtable
private:
__bucket_type* _M_buckets; //_ Hash_node_base *
size_type _M_bucket_count; // bucket 节点个数
__node_base _M_before_begin; // _NodeAlloc::value_type
size_type _M_element_count; // //hashtable中list节点个数
_RehashPolicy _M_rehash_policy; // rehash策略
__bucket_type _M_single_bucket; // 只需要一个桶用
};
функция begin
Вызывает _M_begin:
Можно представить _M_before_begin как головной узел, тогда первый узел — следующий за ним.
функция end
Поскольку это односвязный список, достаточно вернуть последний.
функции size и empty
size_type
size() const noexcept
{ return _M_element_count; }
bool
empty() const noexcept
{ return size() == 0; }
Число корзин
Вычисление коэффициента загрузки
Текущее число элементов делится на число корзин
float
load_factor() const noexcept
{
return static_cast<float>(size()) / static_cast<float>(bucket_count());
}
Вычисление индекса корзины
Получить индекс корзины по переданному key.
size_type
bucket(const key_type& __k) const
{ return _M_bucket_index(__k, this->_M_hash_code(__k)); }
_Hash_code_base есть такая реализация:
А _M_h1 возвращает _H1; когда неизвестно, что такое _H1, мы можем найти в unordered_map, что _Hash=hash<_Key>, поэтому приведённая ниже функция — это математическое выражение:
h1(k) для получения hash code.
Возвращает hash code корзины.
Возвращает индекс корзины.
Определение _M_bucket_index находится далее в том же файле:
size_type
_M_bucket_index(const key_type& __k, __hash_code __c) const
{ return __hash_code_base::_M_bucket_index(__k, __c, _M_bucket_count); }
_M_bucket_index в __hash_code_base — находим его в bits/hashtable_policy.h:
std::size_t
_M_bucket_index(const _Key&, __hash_code __c,
std::size_t __n) const
{ return _M_h2()(__c, __n); }
unordered_map можно найти, что _H2 по умолчанию — _Mod_range_hashing; смотрим эти исходники:
struct _Mod_range_hashing
{
typedef std::size_t first_argument_type;
typedef std::size_t second_argument_type;
typedef std::size_t result_type;
result_type
operator()(first_argument_type __num,
second_argument_type __den) const noexcept
{ return __num % __den; }
};
h2(c,n).
Поэтому математическое выражение для получения индекса корзины через bucket выше:
На деле окончательно получается:
Это и есть вычисление индекса корзины.Похожие темы¶
- Анализ исходников STL array | C++那些事
- Глубокий анализ STL string | C++那些事
- Разбор вопросов собеседования про STL string | C++那些事
- Анализ исходников STL deque | C++那些事
- Оптимизация пустого базового класса EBO в STL | C++那些事
- Реализация хеш-таблицы своими руками | C++那些事
- Анализ исходников STL итераторов | C++那些事
- Анализ исходников STL list | C++那些事
- STL map и multimap | C++那些事
- Анализ исходников STL queue и stack | C++那些事