web-dev-qa-db-ja.com

HashTableのパフォーマンスの問題に興味がある

Haskellのハッシュテーブルにパフォーマンスの問題があることを読みました(2006年の Haskell-Cafe および2009年の Flying Frog Consultancyのブログ )。Haskellが好きなので心配していました。

それは一年前のことですが、今(2010年6月)の状況はどうですか? 「ハッシュテーブルの問題」はGHCで修正されましたか?

51

問題は、ガベージコレクターがポインターの可変配列(「ボックス化された配列」)をトラバースして、割り当てを解除する準備ができている可能性のあるデータへのポインターを探す必要があることでした。ボックス化された可変配列は、ハッシュテーブルを実装するための主要なメカニズムであるため、特定の構造でGCトラバーサルの問題が発生しました。これは多くの言語に共通です。症状は、過剰なガベージコレクションです(GCに費やされる時間の最大95%)。

修正は GCで「カードマーキング」を実装する ポインタの可変配列に対して2009年後半に発生しました。Haskellでポインタの可変配列を使用するときに過剰なGCが表示されないようにする必要があります。単純なベンチマークでは、大きなハッシュのハッシュテーブル挿入が10倍向上しました。

GCウォーキングの問題は、 純粋に関数型の構造 にも、ボックス化されていない配列(ほとんどのデータのように 並列配列 、または ベクトル -のような配列)にも影響しないことに注意してください、Haskellで。Cヒープに格納されているハッシュテーブルにも影響しません( judy など)。つまり、必須のハッシュテーブルを使用しない日常のHaskellerには影響しませんでした。

Haskellでハッシュテーブルを使用している場合、今は問題が発生しないはずです。たとえば、これは、ハッシュに1,000万intを挿入する単純なハッシュテーブルプログラムです。元の引用にはコードやベンチマークが表示されていないため、ベンチマークを実行します。

import Control.Monad
import qualified Data.HashTable as H
import System.Environment

main = do
  [size] <- fmap (fmap read) getArgs
  m <- H.new (==) H.hashInt
  forM_ [1..size] $ \n -> H.insert m n n
  v <- H.lookup m 100
  print v

GHC 6.10.2では、修正前に、1,000万intを挿入します。

$ time ./A 10000000 +RTS -s
...
47s.

GHC 6.13では、修正後:

./A 10000000 +RTS -s 
...
8s

デフォルトのヒープ領域を増やす:

./A +RTS -s -A2G
...
2.3s

ハッシュテーブルの回避とIntMapの使用:

import Control.Monad
import Data.List
import qualified Data.IntMap as I
import System.Environment

main = do
  [size] <- fmap (fmap read) getArgs
  let k = foldl' (\m n -> I.insert n n m) I.empty [1..size]
  print $ I.lookup 100 k

そして、次のようになります。

$ time ./A 10000000 +RTS -s        
./A 10000000 +RTS -s
6s

または、代わりに、judy配列(外部関数インターフェイスを介してCコードを呼び出すHaskellラッパー)を使用します。

import Control.Monad
import Data.List
import System.Environment
import qualified Data.Judy as J

main = do
  [size] <- fmap (fmap read) getArgs
  j <- J.new :: IO (J.JudyL Int)
  forM_ [1..size] $ \n -> J.insert (fromIntegral n) n j
  print =<< J.lookup 100 j

これを実行して、

$ time ./A 10000000 +RTS -s
...
2.1s

したがって、ご覧のとおり、ハッシュテーブルに関するGCの問題は修正済みであり、は常に他のライブラリと完全に適したデータ構造。要約すると、これは問題ではありません。

注:2013年の時点では、おそらく hashtables パッケージを使用する必要があります。これは 可変ハッシュテーブルの範囲 をネイティブにサポートします。

135
Don Stewart

このような質問は、実際には実験によってのみ解決できます。しかし、実験をする時間やお金がない場合は、他の人にどう思うか尋ねる必要があります。その際、出典を検討し、提供された情報が何らかの方法でレビューまたは精査されているかどうかを検討することをお勧めします。

Jon Harropは、Haskellに関するいくつかの興味深い主張を進めました。 Haskell、LISP、およびその他の関数型言語におけるHarropの専門知識の証拠をGoogleグループやその他の場所で検索することをお勧めします。また、Haskellのパトリシアの木に関するChrisOkasakiとAndyGillの作品を読んで、彼らの専門知識がどのように評価されているかを確認することもできます。また、第三者によってチェックされたクレームがある場合は、そのクレームを見つけることもできます。次に、さまざまな関数型言語のパフォーマンスに関するさまざまな人々の主張をどれほど真剣に受け止めるかを自分で決めることができます。

ああ、そしてトロルに餌をやらないでください。


P.S.あなたがあなた自身の実験をすることはかなり合理的ですが、信頼できるドン・スチュワートはいくつかの素晴らしいマイクロベンチマークを提示します彼の細かい答えなので、おそらく必要ではありません。ドンの答えの補遺は次のとおりです。


補遺:AMD Phenom 9850 BlackEditionでDonStewartのコードを使用し、2.5GHz、4GB RAM、32ビットモード、ghc -O

  • デフォルトのヒープでは、IntMapはハッシュテーブルよりも40%高速です。
  • 2Gヒープを使用すると、ハッシュテーブルはIntMapよりも40%高速になります。
  • デフォルトのヒープで1,000万個の要素に移動すると、IntMapはハッシュテーブル(CPU時間)よりも4倍速いまたは2倍速い実時間。

この結果には少し驚いていますが、機能的なデータ構造がかなりうまく機能していることを確信しています。そして、コードが使用される実際の条件下でコードをベンチマークすることは本当に有益であると私は信じています。

26
Norman Ramsey

つまり、最新のGHCで修正されたとしても、Haskellは競争効率の高い辞書(可変または不変)を提供することができません。

Haskellのハッシュテーブルは C++や.NETなどの代替手段よりも32倍遅い GHC6.10でした。これは、 GHC 6.12.2で修正されたGHCガベージコレクターのパフォーマンスバグ が原因の1つです。しかし、Simon Marlowの結果は、パフォーマンスが5倍向上しただけであり、Haskellのハッシュテーブルは他のほとんどの方法よりも何倍も遅くなっています。

純粋関数型の代替案も、まともなハッシュテーブルよりもはるかに低速です。たとえば、 HaskellのIntMapは.NETのハッシュテーブルよりも10倍遅い

F#2010および 最新のHaskell Platform 2010.2.0. (昨日リリースされました!)を使用して、32ビットWindowsVistaを実行しているこの2.0GHzE5405XeonでGHC6.12.3を使用して、20M int-> intバインディングを空のハッシュテーブルでは、Haskellがすべてのコアを書き込むため、HaskellはF#よりもリアルタイムで29倍遅く、CPU時間に関しては200倍以上遅いことがわかります。

GHC 6.12.3 Data.HashTable:42.8s(new!)
。NETハッシュテーブル:1.47s

短命のマイクロベンチマークのみを実行する場合は、ドン・スチュワートが上記で示唆しているように、GHCガベージコレクターを無効にすることができます。この特定のプログラムでは決して埋められないほど大きな保育園の世代を求めることで、彼はハスケルハッシュテーブルの時間をここでわずか1.5秒に短縮しました。ただし、これにより、ナーサリー世代を持つという全体的なポイントが完全に損なわれ、新しく割り当てられた値がキャッシュ内で常にコールドになるため、他のコードのパフォーマンスが大幅に低下します(これが、ナーサリー世代が通常L2キャッシュのサイズである理由です。これよりも桁違いに小さい)。

6
Jon Harrop