簡體   English   中英

我可以在 R 中使用列表作為哈希嗎? 如果是這樣,為什么這么慢?

[英]Can I use a list as a hash in R? If so, why is it so slow?

在使用 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() 更好的替代方案?

根本原因是具有命名元素的 R 列表未經過哈希處理。 哈希查找是 O(1),因為在插入期間使用哈希函數將鍵轉換為整數,然后將值放入空間hash(key) % num_spots of an array num_spots long(這是一個很大的簡化,避免了處理碰撞的復雜性)。 鍵的查找只需要散列鍵以找到值的位置(這是 O(1),而不是 O(n) 數組查找)。 R 列表使用 O(n) 的名稱查找。

正如德克所說,使用散列包。 一個巨大的限制是它使用環境(散列)和覆蓋[方法來模仿散列表。 但是一個環境不能包含另一個環境,所以你不能用散列函數嵌套散列。

前一段時間,我致力於在 C/R 中實現一個可以嵌套的純哈希表數據結構,但是當我從事其他工作時,它在我的項目中擱淺了。 不過如果有就好了:-)

您可以嘗試 environments 和/或 Christopher Brown 的散列包(恰好在引擎蓋下使用環境)。

您的代碼非常不像 R,這也是它如此緩慢的原因之一。 我沒有優化下面的代碼以獲得最大速度,只有 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 系列命令使這更自然,因為該命令旨在表示一個函數對某種列表的應用,而不是通用循環(是的,我知道apply可以用於多個命令)。

我有點 R hack,但我是經驗主義者,所以我將分享我觀察到的一些事情,讓那些對 R 有更深入理論理解的人闡明原因。

  • R 使用標准流似乎比 Perl 慢得多。 由於 stdin 和 stout 在 Perl 中更常用,我假設它對它如何做這些事情進行了優化。 所以在 RI 中發現使用內置函數(例如write.table )讀/寫文本要快得多。

  • 正如其他人所說,R 中的矢量運算比循環更快......而且速度,大多數 apply() 系列語法只是循環上的漂亮包裝器。

  • 索引事物比非索引事物工作得更快。 (很明顯,我知道。)data.table 包支持數據框類型對象的索引。

  • 我從來沒有使用過像@Allen 所示的散列環境(而且我從來沒有吸入散列......據你所知)

  • 您使用的某些語法有效,但可以收緊。 我不認為這對速度有任何影響,但代碼更易讀。 我沒有編寫非常緊湊的代碼,但我編輯了一些內容,例如將floor(1000*runif(1))更改為sample(1:1000, n, replace=T) 我不是故意迂腐,我只是按照我從頭開始的方式編寫它。

因此,考慮到這一點,我決定針對我使用索引 data.table 作為查找表創建的“窮人哈希”來測試 @allen 使用的哈希方法(因為它對我來說很新穎)。 我不是 100% 確定 @allen 和我正在做的正是你在 Perl 中所做的,因為我的 Perl 很生疏。 但我認為下面的兩種方法做同樣的事情。 我們都從“散列”中的鍵中采樣第二組鍵,因為這可以防止散列丟失。 您想測試這些示例如何處理哈希欺騙,因為我沒有考慮那么多。

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 

請記住,這仍然比 Perl 代碼慢得多,后者在我的 PC 上似乎在不到一秒的時間內運行 100K 個樣本。

我希望上面的例子有所幫助。 如果你有任何關於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 代碼!

但是一個環境不能包含另一個環境(引用文斯的回答)。

也許前段時間是這樣的(我不知道)但是這個信息似乎不再准確了:

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

所以環境現在可以制作一個非常有能力的地圖/字典。 也許你會錯過 '[' 運算符,在這種情況下使用 hash 包。

從 hash 包文檔中獲取的這條注釋可能也很有趣:

R 正在慢慢地轉向使用環境的散列的本機實現,(參見 Extract。使用 $ 和 [[ 訪問環境已經有一段時間了,最近對象可以從環境繼承,等等。但是許多使散列/字典成為可能的功能還欠缺很多,比如slice操作,[.

首先,正如 Vince 和 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")

希望這有所幫助。

艾倫

如果您嘗試使用 hash 包在 R 中散列 10,000,000 多個東西,那么構建散列將花費非常非常長的時間。 它使 R 崩潰,盡管事實上數據占用的內存不到我的 1/3。

使用 setkey 的包 data.table 的性能要好得多。 如果您不熟悉 data.table 和 setkey,您可以從這里開始: https ://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html

我意識到最初的問題涉及 10,000 件事,但谷歌幾天前將我定向到這里。 我嘗試使用 hash 包,但遇到了很多困難。 然后我發現這篇博文表明構建哈希可能需要數小時才能完成 1000 萬件以上的事情,這與我的經驗一致:
https://appsilon.com/fast-data-lookups-in-r-dplyr-vs-data-table/?utm_campaign=News&utm_medium=Community&utm_source=DataCamp.com

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM