我是Unix的新手,但我最近意识到,非常简单的Unix命令可以非常非常快速地对大型数据集执行非常简单的操作。我的问题是,为什么这些Unix命令相对于R如此之快?
让我们首先假设数据很大,但不大于计算机上的RAM量。
从计算角度来说,我知道Unix命令可能比R命令更快。然而,我无法想象这能解释整个时差。毕竟,所有基本的R函数,如Unix命令,都是用C/C++等低级语言编写的。
因此,我怀疑速度的提高与I/O有关。虽然我对计算机的工作原理只有基本的了解,但我确实知道,要操作数据,首先要从磁盘读取(假设数据是本地的)。这很慢。然而,无论您使用R函数还是Unix命令来操作数据,大多数都是从磁盘中获取数据的。
因此,我怀疑数据是如何从磁盘读取的,如果这有意义的话,就是时间差的原因。这种直觉正确吗?
谢谢!
更新:抱歉语焉不详。这是有目的的,我希望能笼统地讨论这个想法,而不是集中在一个具体的例子上。
无论如何,我将生成一个计算行数的示例
首先,我将生成一个大数据集。
row = 1e7
col = 50
df<-matrix(rpois(row*col,1),row,col)
write.csv(df,"df.csv")
使用Unix
time wc -l df.csv
real 0m12.261s
user 0m1.668s
sys 0m2.589s
使用R
library(data.table)
system.time({ nrow(fread("df.csv")) })
...
user system elapsed
26.77 1.67 47.07
请注意,已运行/real>用户+系统。这表明CPU正在等待磁盘。
我怀疑R的慢速与读取数据有关。看来我是对的:
system.time(fread("df.csv"))
user system elapsed
34.69 2.81 47.41
我的问题是Unix和R的I/O有何不同。为什么?
我不确定你说的是什么操作,但一般来说,像R这样更复杂的处理系统使用更复杂的内部数据结构来表示被操作的数据,构建这些数据结构可能是一个很大的瓶颈,比grep等Unix命令操作的简单行、字和字符要慢得多。
另一个因素(取决于脚本的设置方式)是,您是在"流模式"下一次处理一件数据,还是将所有内容读入内存。Unix命令往往是为了在管道中操作而编写的,读取一小段数据(通常是一行),处理它,也许写出一个结果,然后转到下一行。另一方面,如果在处理之前将整个数据集读取到内存中,那么即使您有足够的RAM,分配和组织所有必要的内存也可能非常昂贵。
[根据您的附加信息更新]
啊哈。所以你要求R立即将整个文件读入内存。这在很大程度上解释了差异。让我们再谈几件事。
I/O。我们可以考虑三种从文件中读取字符的方法,特别是如果我们所做的处理风格影响了最方便的读取方式。
- 无缓冲的小型随机读取。我们一次向操作系统请求1个或几个字符,并在阅读时对其进行处理
- 未缓冲的块大小的大型读取。我们要求操作人员获取大块内存——通常大小约为1k或8k——并在请求下一块内存之前仔细研究内存中的每一块
- 缓冲读取。我们的编程语言为我们提供了一种从中间缓冲区中请求尽可能多的字符的方法,而内置在该语言中的代码("库"代码)会通过从操作系统中读取大块大小的块来自动保持缓冲区满
现在,需要知道的重要一点是,操作系统更愿意读取大块大小的块。因此,#1可能比2和3慢得多。(我见过10或100的因数。)但没有一个写得好的程序使用#1,所以我们几乎可以忘记它。只要你使用2或3,I/O速度就会大致相同。(在极端情况下,如果你知道自己在做什么,如果可以的话,你可以使用2而不是3来提高效率。)
现在让我们来谈谈每个程序处理数据的方式。wc
基本上有5个步骤:
- 每次读取一个字符。(我可以向你保证,它使用的是方法3。)
- 对于读取的每个字符,在字符数上加一个
- 如果读取的字符是换行符,则在行数上加一
- 如果读取的字符是或不是单词分隔符,请更新字数
- 最后,根据要求打印出行数、单词数和/或字符数
因此,正如您所看到的,这都是I/O和非常简单的基于字符的处理。(唯一复杂的步骤是4。作为练习,我曾经编写了一个版本的wc
,如果用户没有要求所有计数,它就不会在读取循环中执行所有步骤2、3和4。如果您调用wc -c
或wc -l
,我的版本确实运行得更快。但显然,代码要复杂得多。)
另一方面,在R的情况下,事情要复杂得多。首先,您告诉它读取CSV文件。因此,在阅读时,它必须找到分隔行的换行符和分隔列的逗号。这大致相当于wc
所要做的处理。但是,对于它找到的每个数字,它都必须将其转换为一个可以有效处理的内部数字。例如,如果CSV文件中的某个位置出现序列
...,12345,...
R必须读取这些数字(作为单个字符),然后做相当于的数学问题
1 * 10000 + 2 * 1000 + 3 * 100 + 4 * 10 + 5 * 1
以获得值12345。
但还有更多。你让R建一张桌子。表是一种特定的、高度规则的数据结构,它将所有数据排序为严格的行和列,以实现高效查找。为了了解这可能需要做多少工作,让我们使用一个有点牵强的假设现实世界的例子。
假设你是一家调查公司,你的工作是向街上走过的人询问某些问题。但假设问题足够复杂,你需要所有的人同时坐在教室里。(进一步假设人们不介意这种不便。)
但首先你必须建造那间教室。你不确定会有多少人经过,所以你建了一个普通的教室,里面有5排6张桌子,可容纳30人,你把桌子搬进来,人们开始排队,30人排队后,你注意到有第31张,你该怎么办?你可以让他站在后面,但你有点执着于刻板的行列想法,所以你让第31个人等着,然后你很快打电话给建筑商,让他们在第一个教室旁边建第二个30人的教室,现在你可以接受第31个人,实际上还有29个人,总共60个人,但后来你注意到了第61个人。
所以你让他等一下,然后你再次给建筑商打电话,让他们再建造两间教室,所以现在你有了一个由30人组成的漂亮的2x2网格教室,但人们不断来,很快第121人出现了,没有足够的空间,你甚至还没有开始问你的调查问题。
所以你打电话给一些知道如何做钢结构的高级建筑商,让他们在隔壁建造一栋5层楼的大建筑,每层有5间50人的教室,总共50 x 5 x 5=1250张桌子,你让前120个人(他们一直在耐心等待)从旧房间排成队进入新建筑,现在有了第121个人的空间,还有更多的人在他身后,你雇了一些破坏者来拆除旧教室,回收一些材料,人们不断来,很快你的新大楼里就有1250人在等待调查,而1251人刚刚出现。
所以你建造了一座每层100层有1000张办公桌的巨型新摩天大楼,你拆除了旧的5层建筑,但人们不断来,你说你的大数据集有多大?1e7 x 50?所以我认为这座100层的建筑也不够大。(当你完成所有这些后,你要问的唯一"调查问题"是"有多少行?")
尽管看起来很有争议,但这实际上是一个不错的类比,可以用来比喻R在内部构建表以存储数据集
与此同时,鲍勃的折扣调查公司,只能告诉你他调查了多少人,有多少是男性和女性,在哪个年龄段,就在街角,人们正在排队经过,鲍勃在他的剪贴板上记下了计数标记,一旦被调查,人们就走开了,继续他们的生意,鲍勃根本没有浪费时间和金钱建造任何教室。
我对R一无所知,但看看是否有办法提前构建一个空的1e7 x 50矩阵,并将CSV文件读取到其中。你可能会发现这要快得多。R仍然需要进行一些构建,但至少不会有任何错误的开始。