[英]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.