web-dev-qa-db-ja.com

Rでリストをハッシュとして使用できますか?もしそうなら、なぜそれはとても遅いのですか?

Rを使用する前は、かなりの量のPerlを使用していました。 Perlでは、私はしばしばハッシュを使用し、ハッシュのルックアップは一般的にPerlでは高速であると見なされています。

たとえば、次のコードは、最大10000のキーと値のペアをハッシュに入力します。ここで、キーはランダムな文字であり、値はランダムな整数です。次に、そのハッシュで10000回のランダムルックアップを実行します。

#!/usr/bin/Perl -w
use strict;

my @letters = ('a'..'z');

print @letters . "\n";
my %testHash;

for(my $i = 0; $i < 10000; $i++) {
    my $r1 = int(Rand(26));
    my $r2 = int(Rand(26));
    my $r3 = int(Rand(26));
    my $key = $letters[$r1] . $letters[$r2] . $letters[$r3];
    my $value = int(Rand(1000));
    $testHash{$key} = $value;
}

my @keyArray = keys(%testHash);
my $keyLen = scalar @keyArray;

for(my $j = 0; $j < 10000; $j++) {
    my $key = $keyArray[int(Rand($keyLen))];
    my $lookupValue = $testHash{$key};
    print "key " .  $key . " Lookup $lookupValue \n";
}

ますます、Rにハッシュのようなデータ構造を持たせたいと思っています。以下は同等のRコードです。

testHash <- list()

for(i in 1:10000) {
  key.tmp = paste(letters[floor(26*runif(3))], sep="")
  key <- capture.output(cat(key.tmp, sep=""))
  value <- floor(1000*runif(1))
  testHash[[key]] <- value
}

keyArray <- attributes(testHash)$names
keyLen = length(keyArray);

for(j in 1:10000) {
  key <- keyArray[floor(keyLen*runif(1))]
  lookupValue = testHash[[key]]
  print(paste("key", key, "Lookup", lookupValue))
}

コードは同等のことをしているようです。ただし、Perlの方がはるかに高速です。

>time ./perlHashTest.pl
real    0m4.346s
user    **0m0.110s**
sys 0m0.100s

Rとの比較:

time R CMD BATCH RHashTest.R

real    0m8.210s
user    **0m7.630s**
sys 0m0.200s

不一致の理由は何ですか? Rリストでのルックアップは良くありませんか?

リストの長さを100,000に増やし、ルックアップを100,000に増やすと、不一致が誇張されるだけですか? Rのハッシュデータ構造にネイティブlist()よりも優れた代替手段はありますか?

39
stevejb

根本的な理由は、名前付き要素を持つRリストがハッシュされていないことです。ハッシュルックアップはO(1)です。これは、挿入中にハッシュ関数を使用してキーが整数に変換され、配列のスペースhash(key) % num_spotsに値が入れられるためです_num_spots_ long(これはa big単純化され、衝突の処理の複雑さを回避します)。キーのルックアップでは、値の位置を見つけるためにキーをハッシュする必要があります(O(1)に対して、O(n)配列ルックアップ)。RリストはO()である名前ルックアップを使用します。 n)。

Dirkが言うように、ハッシュパッケージを使用します。これに関する大きな制限は、(ハッシュされた)環境を使用し、_[_メソッドをオーバーライドしてハッシュテーブルを模倣することです。ただし、環境に別の環境を含めることはできないため、ハッシュ関数を使用してハッシュをネストすることはできません。

しばらく前に、ネストできる純粋なハッシュテーブルデータ構造をC/Rに実装する作業をしましたが、他の作業をしている間、プロジェクトのバックバーナーに進みました。でもあるといいですね:-)

34
Vince

クリストファー・ブラウンによる環境および/または ハッシュ パッケージを試すことができます(これはたまたま内部の環境を使用します)。

18

あなたのコードは非常にRに似ておらず、それがとても遅い理由の1つです。以下のコードを最大速度用に最適化しておらず、R'nessのみを最適化しています。

n <- 10000

keys <- matrix( sample(letters, 3*n, replace = TRUE), nrow = 3 )
keys <- apply(keys, 2, paste0, collapse = '')
value <- floor(1000*runif(n))
testHash <- as.list(value)
names(testHash) <- keys

keys <- sample(names(testHash), n, replace = TRUE)
lookupValue = testHash[keys]
print(data.frame('key', keys, 'lookup', unlist(lookupValue)))

印刷を除いてほぼ瞬時に動作する私のマシン。あなたのコードはあなたが報告したのとほぼ同じ速度で実行されました。それはあなたが望むことをしていますか? nを10に設定し、出力とtestHashを見て、それであるかどうかを確認できます。

構文に関する注意:上記のapplyは単なるループであり、Rでは低速です。これらのapplyファミリコマンドのポイントは表現力です。以下のコマンドの多くは、applyを使用してループ内に配置でき、それがforループの場合は誘惑になります。 Rでは、ループからできるだけ多くを取り出します。 apply familyコマンドを使用すると、これがより自然になります。これは、コマンドが、汎用ループではなく、ある種のリストへの1つの関数の適用を表すように設計されているためです(はい、applyを複数の関数で使用できることはわかっていますコマンド)。

11
John

私は少しRハックですが、私は経験論者なので、私が観察したいくつかのことを共有し、Rのより理論的な理解を持つ人々にその理由を明らかにさせます。

  • Rは、Perlよりも標準ストリームを使用するとはるかに遅いようです。 stdinとstoutはPerlではるかに一般的に使用されているので、これらのことをどのように行うかについて最適化されていると思います。したがって、Rでは、組み込み関数(_write.table_など)を使用してテキストを読み書きする方がはるかに高速であることがわかります。

  • 他の人が言っているように、Rのベクトル演算はループよりも高速です...そしてw.r.t.速度、ほとんどのapply()ファミリの構文は、ループのかなりのラッパーです。

  • インデックス付きのものは、インデックスなしよりも高速に動作します。 (明らかに、私は知っています。)data.tableパッケージは、データフレームタイプオブジェクトのインデックス作成をサポートします。

  • 私は@Allenのようなハッシュ環境を使用したことがありません(そして、あなたが知っている限り、ハッシュを吸入したことはありません...)

  • 使用した構文の一部は機能しますが、厳密にすることができます。これは速度にとってそれほど重要ではないと思いますが、コードはもう少し読みやすくなっています。あまりタイトなコードは書きませんが、floor(1000*runif(1))sample(1:1000, n, replace=T)に変更するなどの編集を行いました。衒学者になるつもりはありません。最初から書くように書いただけです。

そこで、そのことを念頭に置いて、@ allenが使用したハッシュアプ​​ローチを(私にとっては斬新であるため)、インデックス付きdata.tableをルックアップテーブルとして使用して作成した「貧乏人のハッシュ」に対してテストすることにしました。私のPerlはかなり錆びているので、@ allenと私が行っていることがPerlで行ったこととまったく同じであるかどうかは100%わかりません。しかし、私はthink以下の2つの方法は同じことをします。ハッシュミスを防ぐため、「ハッシュ」内のキーから2番目のキーセットをサンプリングします。私はあまり考えていなかったので、これらの例がハッシュ重複をどのように処理するかをテストしたいと思うでしょう。

_require(data.table)

dtTest <- function(n) {

  makeDraw <- function(x) paste(sample(letters, 3, replace=T), collapse="")
  key <- sapply(1:n, makeDraw)
  value <- sample(1:1000, n, replace=T)

  myDataTable <- data.table(key, value,  key='key')

  newKeys <- sample(as.character(myDataTable$key), n, replace = TRUE)

  lookupValues <- myDataTable[newKeys]

  strings <- paste("key", lookupValues$key, "Lookup", lookupValues$value )
  write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F )
}
_

_hashTest <- function(n) {

  testHash <- new.env(hash = TRUE, size = n)

  for(i in 1:n) {
    key <- paste(sample(letters, 3, replace = TRUE), collapse = "")
    assign(key, floor(1000*runif(1)), envir = testHash)
  }

  keyArray <- ls(envir = testHash)
  keyLen <- length(keyArray)

  keys <- sample(ls(envir = testHash), n, replace = TRUE)
  vals <- mget(keys, envir = testHash)

  strings <- paste("key", keys, "Lookup", vals )
  write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F )

  }
_

100,000回の描画を使用して各メソッドを実行すると、次のようになります。

_> system.time(  dtTest(1e5))
   user  system elapsed 
  2.750   0.030   2.881 
> system.time(hashTest(1e5))
   user  system elapsed 
  3.670   0.030   3.861 
_

これは、私のPCでは1秒未満で100Kサンプルを実行しているように見えるPerlコードよりもかなり遅いことに注意してください。

上記の例がお役に立てば幸いです。また、whyについて質問がある場合は、@ allen、@ vince、および@dirkが回答できる可能性があります;)

上記を入力した後、@ johnが行ったことをテストしていないことに気付きました。それでは、なんと、3つすべてを実行しましょう。コードを@johnからwrite.table()を使用するように変更しました。彼のコードは、次のとおりです。

_johnsCode <- function(n){
  keys = sapply(character(n), function(x) paste(letters[ceiling(26*runif(3))],
    collapse=''))
  value <- floor(1000*runif(n))
  testHash <- as.list(value)
  names(testHash) <- keys

  keys <- names(testHash)[ceiling(n*runif(n))]
  lookupValue = testHash[keys]

  strings <- paste("key", keys, "Lookup", lookupValue )
  write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F )
}
_

および実行時間:

_> system.time(johnsCode(1e5))
   user  system elapsed 
  2.440   0.040   2.544 
_

そして、あなたはそれを持っています。 @johnはタイト/高速のRコードを書きます!

10
JD Long

ただし、環境に別の環境を含めることはできません(Vinceの回答から引用)。

たぶんそれは少し前のことでしたが(私にはわかりません)、この情報はもう正確ではないようです:

> d <- new.env()
> d$x <- new.env()
> d$x$y = 20
> d$x$y
[1] 20

したがって、環境は今ではかなり有能なマップ/辞書を作成します。 '['演算子を見逃してしまうかもしれません。その場合は、ハッシュパッケージを使用してください。

ハッシュパッケージのドキュメントから取られたこのメモも興味深いかもしれません:

Rは、環境を使用したハッシュのネイティブ実装にゆっくりと移行しています(cf. Extract。$および[[を使用した環境へのアクセスは、しばらくの間利用可能であり、最近、オブジェクトは環境などから継承できます。しかし、ハッシュ/辞書を作成する多くの機能スライス操作など、まだまだ足りないものが多い[。

6
memeplex

まず、Vince and Dirkが言ったように、サンプルコードではハッシュを使用していません。 Perlの例の直訳は次のようになります

#!/usr/bin/Rscript
testHash <- new.env(hash = TRUE, size = 10000L)
for(i in 1:10000) {
  key <- paste(sample(letters, 3, replace = TRUE), collapse = "")
  assign(key, floor(1000*runif(1)), envir = testHash)
}

keyArray <- ls(envir = testHash)
keyLen <- length(keyArray)

for(j in 1:10000) {
  key <- keyArray[sample(keyLen, 1)]
  lookupValue <- get(key, envir = testHash)
  cat(paste("key", key, "Lookup", lookupValue, "\n"))
}

これは私のマシン上で非常に高速に実行され、主にセットアップです。 (試して、タイミングを投稿してください。)

しかし、ジョンが言ったように、本当の問題は、Rのベクトル(Perlのマップのような)を考えなければならないことであり、彼の解決策はおそらく最良です。ハッシュを使用したい場合は、検討してください

keys <- sample(ls(envir = testHash), 10000, replace = TRUE)
vals <- mget(keys, envir = testHash)

上記と同じセットアップの後、これは私のマシンではほぼ瞬時に行われます。それらをすべて印刷するには、

cat(paste(keys, vals), sep="\n")

これが少し役立つことを願っています。

アラン

4