我可以在R中使用列表作为哈希吗?如果是这样,为什么这么慢?

时间:2010-08-12 17:38:45

标签: r perl hash

在使用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中的哈希数据结构是否比本机列表()更好?

7 个答案:

答案 0 :(得分:33)

根本原因是带有命名元素的R列表未经过哈希处理。散列查找是O(1),因为在插入期间,使用散列函数将键转换为整数,然后将值放在数组hash(key) % num_spots的空间num_spots中(这是一个<强大>大简化并避免处理碰撞的复杂性)。查找密钥只需要散列密钥以找到值的位置(即O(1),而不是O(n)数组查找)。 R列表使用名称查找,即O(n)。

正如Dirk所说,使用哈希包。这方面的一个巨大限制是它使用环境(经过哈希处理)和覆盖[方法来模仿哈希表。但是环境不能包含其他环境,因此您不能使用哈希函数嵌套哈希。

前段时间我在C / R中实现了一个可以嵌套的纯哈希表数据结构,但是当我处理其他事情时,它继续在我的项目上运行。尽管如此,这将是很好的: - )

答案 1 :(得分:18)

您可以尝试克里斯托弗·布朗的环境和/或hash包(碰巧使用引擎盖下的环境)。

答案 2 :(得分:11)

你的代码非常不像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的循环中,如果它是一个for循环,那将是诱惑。在R中尽可能多地从循环中取出。使用apply family命令使这更自然,因为该命令旨在将一个函数的应用程序表示为某种类型的列表而不是通用循环(是的,我知道apply可以用于多个命令)。

答案 3 :(得分:10)

我有点像R hack,但我是一名经验主义者,所以我会分享一些我观察过的事情,让那些对R有更多理论认识的人能够解释为什么。

  • 使用标准时,R似乎要慢得多 溪流比Perl。既然斯坦丁和 粗壮是更常用的 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作为查找表创建的“穷人的哈希”。我并不是100%确定@allen和我正在做的正是你在Perl中所做的,因为我的Perl非常生疏。但我认为以下两种方法做同样的事情。我们都从'hash'中的键中对第二组键进行采样,因为这可以防止哈希未命中。你想测试这些例子如何处理哈希欺骗,因为我没有多想。

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代码慢得多,Perl代码在我的电脑上似乎在一秒钟内运行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代码!

答案 4 :(得分:6)

  

但环境不能包含其他环境(引自Vince的回答)。

也许就是那段时间(我不知道),但这些信息似乎不再准确:

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

因此环境现在可以制作出非常强大的地图/字典。也许你会错过'['运算符,在这种情况下使用哈希包。

从哈希包文档中获取的这个注释也可能是有意义的:

  

R正在慢慢转向使用哈希的本机实现   enviroments,(参见Extract。使用$和[访问环境]   已经有一段时间了,最​​近的对象可以继承   环境等。但许多功能,使哈希/词典   仍然缺乏伟大的,例如切片操作,[。

答案 5 :(得分:4)

首先,正如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中的map),而他的解决方案可能是最好的。如果您确实想使用哈希值,请考虑

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

在完成与上面相同的设置后,在我的机器上接近瞬时。要打印它们,请尝试

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

希望这有点帮助。

阿伦

答案 6 :(得分:0)

如果您尝试使用哈希包对R中的10,000,000+事物进行哈希,那么构建哈希将花费非常长的时间。尽管数据占用的内存不足我的内存,但它还是使R崩溃了。

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

我意识到最初的问题涉及10,000种事物,但几天前google将我引导到这里。我尝试使用哈希程序包,并且遇到了很多困难。然后,我发现了这篇博客文章,该文章表明构建散列可能要花费数小时才能完成1000万以上的工作,这与我的经验保持一致:
https://appsilon.com/fast-data-lookups-in-r-dplyr-vs-data-table/?utm_campaign=News&utm_medium=Community&utm_source=DataCamp.com