在R会话中管理可用内存的技巧

543
人们用什么技巧来管理交互式R会话中可用的内存?我使用以下函数[基于Petr Pikal和David Hinds在2004年发布的r-help列表]来列出(和/或排序)最大的对象,并偶尔rm()其中一些。但到目前为止,最有效的解决方案是……在64位Linux下运行,内存充足。
还有其他不错的技巧要分享吗?请每个帖子分享一个。
# improved list of objects
.ls.objects <- function (pos = 1, pattern, order.by,
                        decreasing=FALSE, head=FALSE, n=5) {
    napply <- function(names, fn) sapply(names, function(x)
                                         fn(get(x, pos = pos)))
    names <- ls(pos = pos, pattern = pattern)
    obj.class <- napply(names, function(x) as.character(class(x))[1])
    obj.mode <- napply(names, mode)
    obj.type <- ifelse(is.na(obj.class), obj.mode, obj.class)
    obj.size <- napply(names, object.size)
    obj.dim <- t(napply(names, function(x)
                        as.numeric(dim(x))[1:2]))
    vec <- is.na(obj.dim)[, 1] & (obj.type != "function")
    obj.dim[vec, 1] <- napply(names, length)[vec]
    out <- data.frame(obj.type, obj.size, obj.dim)
    names(out) <- c("Type", "Size", "Rows", "Columns")
    if (!missing(order.by))
        out <- out[order(out[[order.by]], decreasing=decreasing), ]
    if (head)
        out <- head(out, n)
    out
}
# shorthand
lsos <- function(..., n=10) {
    .ls.objects(..., order.by="Size", decreasing=TRUE, head=TRUE, n=n)
}

1
注意,我并不怀疑它,但这有什么用呢?我对R中的内存问题还很陌生,但最近遇到了一些问题(这就是我在搜索这篇文章的原因:) - 所以我刚刚开始接触所有这些。这对我的日常工作有什么帮助吗? - Matt Bannert
5
如果想要查看函数内的对象,必须使用:lsos(pos = environment()),否则只会显示全局变量。要写入标准错误输出:write.table(lsos(pos=environment()), stderr(), quote=FALSE, sep='\t')。 - Michael Kuhn
这是一个很棒的函数。但是,假设我想要清除我的工作空间,我可以通过 "rm(list=ls())" 来实现,但这样会导致我失去这个函数。有没有办法使用类似于这样的通用命令来保留一些东西,使其不被删除? - cianius
3
这个功能已经被打包在 multilevelPSA 包中了。虽然这个包是为其他用途而设计的,但你可以通过以下方式使用该函数,而无需加载整个包:requireNamespace(multilevelPSA); multilevelPSA::lsos(...)。或者在 Dmisc 中找到该功能(不在 CRAN 上)。 - krlmlr
1
如果数据集的大小可控,我通常会进入R Studio>环境>网格视图。在这里,您可以根据大小查看和排序当前环境中的所有项目。 - kRazzy R
显示剩余6条评论
28个回答

210

请确保您将工作记录在可重复执行的脚本中。定期重新打开 R,然后source()您的脚本。这样可以清理掉不再使用的任何内容,并且额外受益的是测试了您的代码。


61
我的策略是按照load.R和do.R来分割我的脚本,其中load.R可能需要相当长的时间从文件或数据库加载数据,并对这些数据进行任何最基本的预处理/合并。load.R的最后一行是保存工作空间状态的命令。然后,do.R是我用来构建分析函数的草稿本。我经常重新加载do.R(根据需要重新加载来自load.R的工作空间状态)。 - Josh Reich
34
这是一个不错的技巧。当文件需要按照一定顺序运行时,我通常会在文件名前加上数字:1-load.r2-explore.r3-model.r - 这样其他人就能明确地知道它们之间存在某种顺序。 - hadley
4
我非常支持这个想法。我曾经教过几个人使用 R 语言,这也是我首先告诉他们的事情之一。对于任何一种开发中包含 REPL 和文件编辑的语言(例如 Python),这也适用。 rm(ls = list())和 source()也可以运行,但重新打开会话更好(也会清除包)。 - Vince
65
最受欢迎的答案涉及重新启动R这一事实,是对R最糟糕的批评。 - sds
7
@MartínBel 只能删除在全局环境中创建的对象,它无法卸载包、S4对象或许多其他东西。 - hadley
显示剩余10条评论

172

123

在推特上看到这个帖子,认为Dirk的这个功能非常棒!在JD Long的回答(链接)的基础上,我会这样做以便用户更易读懂:

# improved list of objects
.ls.objects <- function (pos = 1, pattern, order.by,
                        decreasing=FALSE, head=FALSE, n=5) {
    napply <- function(names, fn) sapply(names, function(x)
                                         fn(get(x, pos = pos)))
    names <- ls(pos = pos, pattern = pattern)
    obj.class <- napply(names, function(x) as.character(class(x))[1])
    obj.mode <- napply(names, mode)
    obj.type <- ifelse(is.na(obj.class), obj.mode, obj.class)
    obj.prettysize <- napply(names, function(x) {
                           format(utils::object.size(x), units = "auto") })
    obj.size <- napply(names, object.size)
    obj.dim <- t(napply(names, function(x)
                        as.numeric(dim(x))[1:2]))
    vec <- is.na(obj.dim)[, 1] & (obj.type != "function")
    obj.dim[vec, 1] <- napply(names, length)[vec]
    out <- data.frame(obj.type, obj.size, obj.prettysize, obj.dim)
    names(out) <- c("Type", "Size", "PrettySize", "Length/Rows", "Columns")
    if (!missing(order.by))
        out <- out[order(out[[order.by]], decreasing=decreasing), ]
    if (head)
        out <- head(out, n)
    out
}
    
# shorthand
lsos <- function(..., n=10) {
    .ls.objects(..., order.by="Size", decreasing=TRUE, head=TRUE, n=n)
}

lsos()

这将导致类似以下的结果:
                      Type   Size PrettySize Length/Rows Columns
pca.res                 PCA 790128   771.6 Kb          7      NA
DF               data.frame 271040   264.7 Kb        669      50
factor.AgeGender   factanal  12888    12.6 Kb         12      NA
dates            data.frame   9016     8.8 Kb        669       2
sd.                 numeric   3808     3.7 Kb         51      NA
napply             function   2256     2.2 Kb         NA      NA
lsos               function   1944     1.9 Kb         NA      NA
load               loadings   1768     1.7 Kb         12       2
ind.sup             integer    448  448 bytes        102      NA
x                 character     96   96 bytes          1      NA

注意:我添加的主要部分是(再次从JD的答案中适应):

obj.prettysize <- napply(names, function(x) {
                           print(object.size(x), units = "auto") })

1
这个函数能否被添加到dplyr或其他关键包中? - userJT
1
值得注意的是(至少在base-3.3.2中),现在不再需要“capture.output”,而且“obj.prettysize <- napply(names, function(x) {format(utils::object.size(x), units = "auto") })”会产生干净的输出。实际上,如果不去掉它,输出中会出现不必要的引号,即“[1]“792.5 Mb””而不是“792.5 Mb”。 - runr
@Nutle 很好,我已经相应地更新了代码 :) - Tony Breyal
我还会将obj.class <- napply(names, function(x) as.character(class(x))[1])更改为obj.class <- napply(names, function(x) class(x)[1]),因为现在class始终返回一个字符向量(基于3.5.0)。 - DeltaIV
有什么想法可以将“改进的对象列表”指向特定的环境? - pdeli

52

在我将数据框传递给回归函数的data=参数时,我会积极使用subset参数,仅选择所需变量。如果我忘记在公式和select=向量中添加变量,可能会导致一些错误,但由于减少了对象的复制并显著减少了内存占用,因此仍然节省了大量时间。比方说,我有4百万条记录和110个变量(确实如此)。例如:

# library(rms); library(Hmisc) for the cph,and rcs functions
Mayo.PrCr.rbc.mdl <- 
cph(formula = Surv(surv.yr, death) ~ age + Sex + nsmkr + rcs(Mayo, 4) + 
                                     rcs(PrCr.rat, 3) +  rbc.cat * Sex, 
     data = subset(set1HLI,  gdlab2 & HIVfinal == "Negative", 
                           select = c("surv.yr", "death", "PrCr.rat", "Mayo", 
                                      "age", "Sex", "nsmkr", "rbc.cat")
   )            )

通过设定背景和策略:变量gdlab2是一个逻辑向量,用于构造数据集中那些在一堆实验室测试中全部或几乎全部具有正常值的受试者,而HIVfinal是一个字符向量,总结了对HIV的初步和确认性检测结果。


49

我喜欢Dirk的.ls.objects()脚本,但是在大小列中一直要眯眼数字符号。因此,我进行了一些丑陋的黑客攻击,使其以漂亮的格式呈现大小:

.ls.objects <- function (pos = 1, pattern, order.by,
                        decreasing=FALSE, head=FALSE, n=5) {
    napply <- function(names, fn) sapply(names, function(x)
                                         fn(get(x, pos = pos)))
    names <- ls(pos = pos, pattern = pattern)
    obj.class <- napply(names, function(x) as.character(class(x))[1])
    obj.mode <- napply(names, mode)
    obj.type <- ifelse(is.na(obj.class), obj.mode, obj.class)
    obj.size <- napply(names, object.size)
    obj.prettysize <- sapply(obj.size, function(r) prettyNum(r, big.mark = ",") )
    obj.dim <- t(napply(names, function(x)
                        as.numeric(dim(x))[1:2]))
    vec <- is.na(obj.dim)[, 1] & (obj.type != "function")
    obj.dim[vec, 1] <- napply(names, length)[vec]
    out <- data.frame(obj.type, obj.size,obj.prettysize, obj.dim)
    names(out) <- c("Type", "Size", "PrettySize", "Rows", "Columns")
    if (!missing(order.by))
        out <- out[order(out[[order.by]], decreasing=decreasing), ]
        out <- out[c("Type", "PrettySize", "Rows", "Columns")]
        names(out) <- c("Type", "Size", "Rows", "Columns")
    if (head)
        out <- head(out, n)
    out
}

35

这是个好技巧。

另外一个建议是尽可能使用内存高效的对象:例如,使用矩阵而不是数据框。

虽然这并没有真正解决内存管理的问题,但一个重要的函数并不是广为人知的,那就是memory.limit()。你可以使用该命令增加默认值,比如memory.limit(size=2500),其中size以MB为单位。正如Dirk所提到的,你需要使用64位才能真正利用它的优势。


26
这只适用于Windows系统吗? - Christopher DuBois
4
内存限制() [1] 无穷大 警告信息: 'memory.limit()' 仅适用于 Windows - LJT
1
使用tibble而不是data.frame能更好地节省内存吗? - user12059497

33

我非常喜欢Dirk开发的改进对象函数。然而,大部分时间里,仅包含对象名称和大小的更基本输出对我来说已经足够了。这里有一个更简单的函数,具有类似的目标。内存使用可以按字母顺序或按大小排序,可以限制到特定数量的对象,并且可以按升序或降序排序。此外,我经常处理1GB+的数据,所以该函数会相应地更改单位。

showMemoryUse <- function(sort="size", decreasing=FALSE, limit) {

  objectList <- ls(parent.frame())

  oneKB <- 1024
  oneMB <- 1048576
  oneGB <- 1073741824

  memoryUse <- sapply(objectList, function(x) as.numeric(object.size(eval(parse(text=x)))))

  memListing <- sapply(memoryUse, function(size) {
        if (size >= oneGB) return(paste(round(size/oneGB,2), "GB"))
        else if (size >= oneMB) return(paste(round(size/oneMB,2), "MB"))
        else if (size >= oneKB) return(paste(round(size/oneKB,2), "kB"))
        else return(paste(size, "bytes"))
      })

  memListing <- data.frame(objectName=names(memListing),memorySize=memListing,row.names=NULL)

  if (sort=="alphabetical") memListing <- memListing[order(memListing$objectName,decreasing=decreasing),] 
  else memListing <- memListing[order(memoryUse,decreasing=decreasing),] #will run if sort not specified or "size"

  if(!missing(limit)) memListing <- memListing[1:limit,]

  print(memListing, row.names=FALSE)
  return(invisible(memListing))
}

这里是一些示例输出:

> showMemoryUse(decreasing=TRUE, limit=5)
      objectName memorySize
       coherData  713.75 MB
 spec.pgram_mine  149.63 kB
       stoch.reg  145.88 kB
      describeBy    82.5 kB
      lmBandpass   68.41 kB

31

我从不保存R工作空间。我使用导入脚本和数据脚本,并将任何特别大的数据对象输出到文件,这样我总是以全新的工作空间开始,而无需清除大型对象。不过那个功能确实非常好用。


30

很遗憾我没有足够的时间进行全面测试,但是这里有一个我之前没有见过的记忆技巧。对我而言,所需内存减少了50%以上。 当您使用例如read.csv将内容读入R中时,它们需要一定量的内存。 在此之后,您可以使用save("目标文件名",list=ls())命令将其保存下来。 下次打开R时,您可以使用load("目标文件名")命令打开它。 现在内存使用可能已经减少。 如果有人能确认是否与不同数据集产生类似的结果,那就太好了。


4
是的,我也有同样的经历。在我的情况下,内存使用量甚至降至30%。使用了1.5GB的内存,并保存为.RData(〜30MB)。加载.RData后的新会话使用的内存少于500MB。 - f3lix
我尝试使用fread将两个数据集(100MB和2.7GB)加载到data.table中,然后保存为.RData文件。实际上,RData文件确实缩小了约70%,但重新加载后,内存使用量完全相同。本来希望这个技巧可以减少内存占用...我错过了什么吗? - NoviceProg
@NoviceProg 我不认为你错过了什么,但这是一个技巧,我猜它并不适用于所有情况。在我的情况下,重新加载后的内存实际上会像描述的那样减少。 - Dennis Jaheruddin
7
@NoviceProg 有几点需要说明。首先,按照data.table的理念,使用fread比使用read.csv更节省内存,能够更有效地加载文件。其次,人们在这里提到的内存节省主要与R进程的内存大小有关(它会在对象被创建时扩展,在垃圾回收时缩小)。但是,垃圾回收并不总是将所有RAM都释放回操作系统。停止R会话并从存储位置重新加载项目将释放尽可能多的RAM...但如果起始时开销很小...那就没有任何收益。 - russellpierce

28

为了进一步说明常见的频繁重启策略,我们可以使用littler,它允许我们直接从命令行运行简单表达式。以下是一个我有时用来计时不同BLAS进行简单交叉乘积的示例。

 r -e'N<-3*10^3; M<-matrix(rnorm(N*N),ncol=N); print(system.time(crossprod(M)))'

同样地,

 r -lMatrix -e'example(spMatrix)'

通过--packages | -l开关加载Matrix包并运行spMatrix函数的示例。由于R始终从头开始,“这种方法也是软件包开发过程中的一个很好的测试方法。”

最后但同样重要的是,使用“#!/usr/bin/r” shebang-header,R也非常适合在脚本中自动化批处理模式。如果littler不可用(例如在Windows上),可以使用Rscript作为替代。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接