Ruby
hash

RubyのObject#hashについて調べてみた

はじめに

RubyのObject#hashはオブジェクトのハッシュ値を返します。つまり、a.eql?(b) ならば a.hash == b.hashとなります。しかし、公式ドキュメントにも以下のようにあり、異なるプロセス間で同じ値となることは保証されていません。

The hash value for an object may not be identical across invocations or implementations of Ruby.

そこで、なぜ違う値を返すのかを調べてみました。

調査

pry-doc

手始めにpryのshow-sourceコマンドでソースコードを表示します。

$ pry
[1] pry(main)> show-source Object#hash

From: object.c (C Method):
Owner: Kernel
Visibility: public
Number of lines: 13

VALUE
rb_obj_hash(VALUE obj)
{
    VALUE oid = rb_obj_id(obj);
#if SIZEOF_LONG == SIZEOF_VOIDP
    st_index_t index = NUM2LONG(oid);
#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP
    st_index_t index = NUM2LL(oid);
#else
# error not supported
#endif
    return LONG2FIX(rb_objid_hash(index));
}

Objectのidを取得してLongに変換、そしてハッシュ値を求めているようです。
ObjectのidはRubyの実行のたびに振り直されるためハッシュ値が同じにならない理由になりそうですが、Objectのidが同じでもハッシュ値が異なることはあります。

Rubyソースコード

これ以降はRubyのソースコードをみていきます。
対象のバージョンは現時点で最新の2.4.2としました。

hash.c#L245-L249
long
rb_objid_hash(st_index_t index)
{
    return (long)key64_hash(rb_hash_start(index), (uint32_t)prime2);
}

key64_hashは再現性のあるハッシュ関数、prime2は定数でした。
rb_hash_startの中に進みます。

random.c#L1505-L1509
st_index_t
rb_hash_start(st_index_t h)
{
    return st_hash_start(seed.key.hash + h);
}

このseedInit_RandomSeedCoreという関数で初期化されます。
以下のfill_random_seedはその初期化の中で呼び出される関数の1つです。

random.c#L546-L568
static void
fill_random_seed(uint32_t *seed, size_t cnt)
{
    static int n = 0;
    struct timeval tv;
    size_t len = cnt * sizeof(*seed);

    memset(seed, 0, len);

    fill_random_bytes(seed, len, TRUE);

    gettimeofday(&tv, 0);
    seed[0] ^= tv.tv_usec;
    seed[1] ^= (uint32_t)tv.tv_sec;
#if SIZEOF_TIME_T > SIZEOF_INT
    seed[0] ^= (uint32_t)((time_t)tv.tv_sec >> SIZEOF_INT * CHAR_BIT);
#endif
    seed[2] ^= getpid() ^ (n++ << 16);
    seed[3] ^= (uint32_t)(VALUE)&seed;
#if SIZEOF_VOIDP > SIZEOF_INT
    seed[2] ^= (uint32_t)((VALUE)&seed >> SIZEOF_INT * CHAR_BIT);
#endif
}

(実行環境にも依存しますが)Object#hashが異なるプロセス間で違う値となる理由を見つけました。

  • 現在時刻のマイクロ秒(tv.tv_usec)
  • 現在時刻の秒(tv.tv_sec)
  • プロセスID(getpid())

これらの値がseedに使われることで、異なるプロセス間ではObject#hashは異なる値を返すようです。

参考

Ruby-Doc.org <http://ruby-doc.org/core-2.4.2/Object.html>
Ruby Programming Language <https://github.com/ruby/ruby>
pry-docでカジュアルにRubyのソースコードを読む <https://qiita.com/joker1007/items/42f00b12c65bbec0e50a>