コンテンツにスキップ

最終更新:2026-10-12

STL hashtable ハッシュテーブル

0.はじめに

ハッシュテーブルは、unordered_mapやunordered_setなどの基盤コンテナであり、gcc2.9以降、ソースコードの量が大幅に増えました!

今回読むコードも引き続きgcc4.9.1です。コード量が非常に多いので、すべてを展開するのではなく、基盤のハッシュの芸術と技術に焦点を当てて研究します。この2つの言葉、なんだか韻を踏んでいるようですね、哈哈。本文に入りましょう~

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>
{
};

クラステンプレートを学んだことのない人はちんぷんかんぷんで、テンプレートパラメータを数えるだけでも目が回るでしょう。。。

さらにその継承も、一気にこれほど多くの親が出てきます。。。

以下で、その親を1つずつ分析し、その後ハッシュテーブルに戻りましょう。

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つあります:

__detail::_Hash_code_base
__detail::_Hashtable_ebo_helper

この2つのクラスを引き続き追跡しましょう!

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>
}
- 範囲ハッシュを使い(実際には私たちが普段「除留余数法」と呼ぶものです)、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, true>;
- ハッシュ関数と範囲ハッシュ関数を持ち、hash codeをキャッシュしない。

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>
{
};
- 上記のhash codeのキャッシュ

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(デフォルト範囲ハッシュ関数)

h1=hash<key>
次のものがまさにそれです:

h2(h1(key),N)=h1(key)%N
具体的な説明は後で見られます。
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; }
};
structで定義されているので、クラスだと思うかもしれませんが、実際には()演算子をオーバーロードしており、ファンクタです。

上のものをハッシュテーブルのデータ構造に対応させると、皆さんの知っている散列関数:**除留余数法**です。

f(__num) = __num mod __den(__den<=__num)
次に、_Default_ranged_hashです:

struct _Default_ranged_hash { };
これはマーカーとしてのみ使われ、デフォルトですでに計算済みの範囲ハッシュ関数(Default ranged hash function)です:
h(k, N) = h2(h1(k), N),
ここまでで、基盤のハッシュテーブルの散列関数は明らかです。デフォルトではこのようになっています。 そして先ほど触れたマーカーは、型H1とH2のオブジェクトをHに合成すると余分なコピー操作が消費されるため、それを避けるために導入されたものです。

以上で、上述の_H1と_H2の説明は終わりです。それぞれ上の2つの関数に対応しています。

(2) rehash操作

続いて、rehashと呼ばれる比較的重要なものがあります。皆さんrehashはよくご存じでしょう。散列表の衝突がある程度に達したら、keyを適切な位置に再配置する必要があります。ハッシュテーブルの基盤のソースコードではそのようになっており、ここではrehash policyとしてカプセル化されています:

struct _Prime_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计算下一个素数桶
ハッシュ衝突が起きたとき、どのように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;
}
ハッシュ衝突が発生したとき、この関数はnより小さくない素数を返して次のバケットとします。

素数表

素数はどのように検索するのでしょうか? 上に__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,
    // 后面还有很多
  }
だからすべてが非常に明確になります。lower_boundを使って上の表の中から、与えられた値n以上の最初の素数を探すのです。

enum { _S_n_primes = sizeof(unsigned long) != 8 ? 256 : 256 + 48 };

要素に対応するバケットの計算

最大負荷率から最小のバケット数を算出し、そのバケット数に基づいて各要素に対応する最小の素数バケットを計算します。

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(); }
};

ノードイテレータ:下のコードを読み研究することで、2つの点が学べます: - 第1に:usingの使用 - hashtableのイテレータはforward_iteratorに属する - ++、--、*、->の4つの演算子がオーバーロードされている

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を使ってkeyのhash codeを計算し、その後、除留余数法の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;  // 只需要一个桶用
};
hashtableのいくつかの重要な関数:

begin関数

iterator
begin() noexcept
{ return iterator(_M_begin()); }

_M_beginを呼び出します:

_M_before_beginはheadノードのように想像でき、最初のノードはその次のノードです。

__node_type*
_M_begin() const
{ return static_cast<__node_type*>(_M_before_begin._M_nxt); }

end関数

単方向連結リストなので、最後を返せばよいだけです。

iterator
end() noexcept
{ return iterator(nullptr); }

sizeとempty関数

size_type
size() const noexcept
{ return _M_element_count; }

bool
empty() const noexcept
{ return size() == 0; }

バケット数

size_type
bucket_count() const noexcept
{ return _M_bucket_count; }

負荷率の計算

現在の要素数をバケット数で割ったものです

float
load_factor() const noexcept
{
    return static_cast<float>(size()) / static_cast<float>(bucket_count());
}

バケットのindex計算

渡されてきたkeyに基づいてバケットのindexを取得します。

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を返します。

__hash_code
_M_hash_code(const _Key& __k) const
{ return _M_h1()(__k); }

バケットのindexを返します。

_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); }
続いて__hash_code_baseに_M_bucket_indexを探しに行くと、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); }
上述のh1の検索と同様に、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がバケットのindexを取得するのに対応する数学的表現は次の通りです:

h(k,hash(k))=h(k,hash(k),n)=h(k,hash(k)%n)
実際には最終的には次のようになります:

hash(k)%n
これがバケットのindex計算です。

関連トピック

コメント