Try Golang! 効率的なHashmapを実装するためにGoがしたこと

How the Go runtime implements maps efficiently

Takuo
VELTRA Engineering
7 min readApr 24, 2018

--

先日、Go Conference 2018 Springに参加してきました。キーノートはDave Cheney氏による、GoのHashmapはどのようにして効率的な実装を達成したのか、について(本記事のサブタイトルに、キーノートのタイトルを借用させていただきました)。復習も兼ねてHashmapの実装を覗いてみたので、簡単にまとめようと思います。

なお、英語のセッションだったので、正しく理解できていない部分があるかもしれません。お気付きの方は、ぜひご指摘ください。

Hashmapに関する他言語の問題点

Dave Cheney氏によると、C++とJavaのHashmapには、それぞれ以下のような問題点があるとのことです。

  • C++: キーと値の組合せが異なるHashmapは、型としても全く別物となる。結果、コンパイル後のプログラムには、それぞれのHashmap実装が別物として含まれることになる。
  • Java: プリミティブ型については、ラッパークラスを用いてHashmapを利用することとなる。結果、参照時のパフォーマンスが劣化する。
  • Java: カスタムクラスをキーとした場合、equalsメソッドとhashCodeメソッドの実装を誤ると、パフォーマンスが大きく劣化する。(場合によっては正しく動作しなくなる)

これらの問題を解消するために、Goではmaptypeというものが利用されています。

Hashmapの型情報を保持するmaptype

では、このmaptypeの実装を見てみましょう。

maptyperuntime/type.go に、キー(key)と値(elem)の型をそれぞれ保持した構造体として定義されています。そしてこの型は、アルゴリズム(alg)というプロパティを持っています。

このtypeAlgは、runtime/alg.goに定義されていて、内容としてはハッシュ値を返す関数(hash)と、等価かどうかを判定する関数(equal)を持つ構造体となっています。

int型やstring型等、もともと定義されている型については、下記のようにhash関数やequal関数が予め用意されています。構造体等は、コンパイル時に自動生成されるようです。構造体をイコールで比較すると中のプロパティの値まで見て判定してくれますが、この時に使用されるのも、この自動生成されたequal関数のようですね。なお、Goではこれらの関数をオーバーライドすることはできません。

実際、runtime/hashmap.goの中でマップにアクセスする際などは、これらの関数が利用されています。なお、val := m[key]のようなコードはシンタックスシュガーで、実際に呼ばれているのは、下記の関数mapaccess1とのことです。(val, ok := m[key]の場合はmapaccess2

実際はhashequalの他にもmaptypeが保持する値を利用していますが、すべての型に対応し、かつ実体がひとつのマップを、ラッパークラスなどを用いずに実現した方法について、概略は掴めたのではないでしょうか。

どれくらい速いのか

色々な条件があると思うので、あくまで参考としてですが、GoとJavaで類似のコードを書いて、実行速度を比較してみました。まずはGoのコードです。値の設定と、キー指定の参照を10000回実施しています。マップの型は、Javaでラッパークラスを使用する必要があるintです。

続いてJavaのコードはこんな感じ。

コンパイルして実行すると、結果はこうなりました。実行時間は多少ばらけますが、Goの方が速いと言っていいのかな、と。まぁそもそもランタイムかましているJavaと比較していいのかは置いといて。。

私自身、普段の業務の中で、言語の実装を深掘りすることはあまりないのですが、今回色々と見ることができ、新鮮で面白かったです。Goを書いている方にとっては実装方法の勉強にもなると思うので、興味がある方は覗いてみてはいかがでしょうか。

--

--

Takuo
VELTRA Engineering

Engineer who likes travel, simple code, and something new