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

Posted

技术标签:

【中文标题】我可以在 R 中使用列表作为哈希吗?如果是这样,为啥这么慢?【英文标题】:Can I use a list as a hash in R? If so, why is it so slow?我可以在 R 中使用列表作为哈希吗?如果是这样,为什么这么慢? 【发布时间】:2011-03-29 01:54:48 【问题描述】:

在使用 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 的做事方式感兴趣。如果是这样,请在您的问题中添加一个相对详细的哈希表用途示例。 顺便说一句,与 perl 相比,目前尚不清楚是什么减慢了速度。它可能是您的字符串生成(在 perl 中速度很快)。此外,您正在做一些非 R 之类的事情......请参阅我的回答。 我认为这是散列,根据下面文斯的回答。我的实际代码涉及遍历一个列表,为列表中的每个项目从带有 Rmysql 的数据库中选择,进行一些计算,然后更新数据库。我可以稍后发布一个更好的例子。谢谢。 此外,即使我的 R 代码不是以类似 R 的方式编写的(尤其是 for 循环),R 和 Perl 中的简单 for 循环也应该以大致相同的速度运行。我希望我的演示能够隔离 R 的 list() 和 Perl 的哈希之间的差异。我实际上试图做的是缓存一些我需要重复使用的临时结果。 【参考方案1】:

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

正如 Dirk 所说,使用 hash 包。一个巨大的限制是它使用环境(散列)和覆盖[ 方法来模拟散列表。但是一个环境不能包含另一个环境,因此您不能使用散列函数嵌套散列。

不久前,我致力于在 C/R 中实现一个可以嵌套的纯哈希表数据结构,但是当我从事其他事情时,它在我的项目中被搁置了​​。要是有就好了:-)

【讨论】:

嘿文斯。感谢您的指针和解释。下次需要时,我会尝试使用该哈希包。手头的任务已经完成,但我相信我会在不久的将来使用它。非常感谢! 不幸的是,这种解释有点过于简单化了——如果你正在索引一个向量,R 将切换到超过某个大小阈值的散列。根据 Simon Urbanek 的说法,“subscript.c@493 有确切的公式”。 谢谢哈德利!我记得 Duncan TL 很久以前就告诉过我,但直到现在我都完全忘记了。我会检查代码并尝试更新我的帖子。我认为这是 R > 2.9,不是吗?【参考方案2】:

您可以尝试环境和/或 Christopher Brown 的 hash 包(恰好在后台使用环境)。

【讨论】:

与仅使用环境相比,hash 包有哪些优势?是否有任何负面的权衡?我传统上使用环境来给我散列 Christopher 在 userR 上做了一个很好的介绍——基本上你得到了所有常用的操作符。【参考方案3】:

您的代码非常不像 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 系列命令使这更自然,因为该命令旨在表示将一个函数应用于某种列表,而不是通用循环(是的,我知道 apply 可以用于多个命令)。

【讨论】:

【参考方案4】:

我有点 R hack,但我是一个经验主义者,所以我会分享一些我观察到的事情,让那些对 R 有更深入理论理解的人阐明原因。

R 使用标准时似乎慢得多 流比 Perl。由于标准输入和 粗壮更常用于 Perl 我认为它有优化 围绕它如何做这些事情。所以在 R I 发现使用内置的读/写文本要快得多 函数(例如write.table)。

正如其他人所说,矢量 R中的操作比 循环......和w.r.t.速度,大多数 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 上,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 代码!

【讨论】:

哇,这真的很酷。感谢您的分析和输入。在我十几岁的时候,我在一家网络托管公司工作,即使经过多年,我的 perl 习惯仍然存在。在 R 中思考!在 R 中思考! :) 我可以在你的机器上以 2s 的速度获得它,如果你愿意的话,甚至可能是 1.5s :) 我只针对 r-ness 进行了优化。例如,sprintf() 可以类似于 paste() 使用,它比 paste() 快很多。此外,显式取消列出键而不是 sapply() 更快。我想我可以继续下去:)。 您可能想查看我修改后的代码。比以前减少了 30% - 50%。【参考方案5】:

但一个环境不能包含另一个环境(引自文斯的回答)。

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

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

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

这个来自哈希包文档的注释可能也很有趣:

R 正在慢慢地使用 环境,(参见提取物。使用 $ 和 [[ 访问环境有 已经有一段时间了,最​​近对象可以继承自 环境等,但许多功能使哈希/字典 还很欠缺,比如切片操作,[.

【讨论】:

【参考方案6】:

首先,正如文斯和德克所说,您没有在示例代码中使用散列。 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"))

它在我的机器上运行得很快,它们主要是设置。 (尝试一下并发布时间。)

但正如 John 所说,真正的问题是您必须在 R 中考虑向量(就像 perl 中的 map),而他的解决方案可能是最好的。如果您确实想使用哈希,请考虑

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

在与上面相同的设置之后,这在我的机器上几乎是即时的。全部打印出来试试

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

希望这会有所帮助。

艾伦

【讨论】:

【参考方案7】:

如果您尝试使用 hash 包在 R 中对 10,000,000 多个事物进行散列,那么构建散列需要很长时间。尽管数据占用的内存不到我内存的 1/3,但它还是让 R 崩溃了。

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

我意识到最初的问题涉及 10,000 件事情,但几天前谷歌将我引导到这里。我尝试使用 hash 包并且遇到了非常困难的时间。然后我发现这篇博客文章表明构建哈希可能需要数小时才能完成 10M+ 的事情,这符合我的经验:https://appsilon.com/fast-data-lookups-in-r-dplyr-vs-data-table/?utm_campaign=News&utm_medium=Community&utm_source=DataCamp.com

【讨论】:

以上是关于我可以在 R 中使用列表作为哈希吗?如果是这样,为啥这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

PHP,可以吗?将密码哈希中的字符串作为主键存储在数据库中?

使用类常量作为 Ruby 哈希键

将哈希表的键转换为字符串列表

我可以将班级成员的哈希码用于班级吗?

如何迁移密码哈希?

读取R中的CSV作为数据帧