人们用什么技巧来管理交互式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)
}
答案 0 :(得分:184)
确保以可重现的脚本记录您的工作。有时,重新打开R,然后source()
你的脚本。你将清除你不再使用的任何东西,并且作为额外的好处将测试你的代码。
答案 1 :(得分:151)
我使用data.table包。使用:=
运算符,您可以:
这些操作都不会复制(可能很大)data.table
,甚至根本不复制一次。
data.table
使用的工作内存要少得多。相关链接:
答案 2 :(得分:106)
在Twitter帖子上看到这个并认为这是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") })
答案 3 :(得分:48)
我喜欢Dirk的.ls.objects()脚本,但我一直眯着眼睛来计算size列中的字符数。所以我做了一些丑陋的黑客,让它以大小的漂亮格式呈现:
.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
}
答案 4 :(得分:48)
在将数据帧传递给回归函数的subset
参数时,我积极使用data=
参数,只选择所需的变量。如果我忘记在公式和select=
向量中添加变量,它确实会导致一些错误,但由于减少了对象的复制并显着减少了内存占用,它仍然节省了大量时间。假设我有包含110个变量的400万条记录(我也是。)示例:
# 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
变量是为数据集中的主体构建的逻辑向量,该数据集具有一堆实验室测试的所有正常或几乎正常的值,{{1}是一个特征向量,总结了HIV的初步和确认测试。
答案 5 :(得分:33)
这是一个好方法。
另一个建议是尽可能使用内存有效的对象:例如,使用矩阵而不是data.frame。
这并没有真正解决内存管理问题,但一个不为人所知的重要功能是memory.limit()。您可以使用此命令memory.limit(size = 2500)增加默认值,其中大小以MB为单位。正如Dirk所提到的,你需要使用64位才能真正利用这一点。
答案 6 :(得分:31)
我非常喜欢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
答案 7 :(得分:30)
不幸的是我没有时间对它进行广泛测试,但这是一个我以前没见过的记忆提示。对我来说,所需的内存减少了50%以上。
当您使用例如read.csv将内容读入R时,它们需要一定量的内存。
在此之后,您可以使用save("Destinationfile",list=ls())
保存它们
下次打开R时,您可以使用load("Destinationfile")
现在内存使用量可能已减少。
如果有人能够确认这是否会产生与不同数据集类似的结果,那将是很好的。
答案 8 :(得分:29)
我从不保存R工作区。我使用导入脚本和数据脚本,并输出任何我不想经常复制到文件的特别大的数据对象。这样我总是从一个新的工作区开始,不需要清理大的物体。这是一个非常好的功能。
答案 9 :(得分:25)
为了进一步说明频繁重启的常见策略,我们可以使用littler,它允许我们直接从命令行运行简单表达式。这是一个我有时用来为一个简单的crossprod计算不同BLAS的例子。
r -e'N<-3*10^3; M<-matrix(rnorm(N*N),ncol=N); print(system.time(crossprod(M)))'
同样地,
r -lMatrix -e'example(spMatrix)'
加载Matrix包(通过--packages | -l开关)并运行spMatrix函数的示例。由于r总是开始'新鲜',这种方法在包开发过程中也是一个很好的测试。
最后但并非最不重要的是,对于使用'#!/ usr / bin / r'shebang-header的脚本中的自动批处理模式,r也很有用。 Rscript是一个替代品,其中littler不可用(例如在Windows上)。
答案 10 :(得分:23)
为了速度和内存目的,当通过一系列复杂的步骤构建大型数据框时,我会定期将它(正在构建的正在进行的数据集)刷新到磁盘,附加到之前的任何内容,以及然后重新启动它。这样,中间步骤仅适用于较小的数据帧(这是好的,例如, rbind 使用较大的对象显着减慢)。当所有中间对象都被删除时,可以在过程结束时读回整个数据集。
dfinal <- NULL
first <- TRUE
tempfile <- "dfinal_temp.csv"
for( i in bigloop ) {
if( !i %% 10000 ) {
print( i, "; flushing to disk..." )
write.table( dfinal, file=tempfile, append=!first, col.names=first )
first <- FALSE
dfinal <- NULL # nuke it
}
# ... complex operations here that add data to 'dfinal' data frame
}
print( "Loop done; flushing to disk and re-reading entire data set..." )
write.table( dfinal, file=tempfile, append=TRUE, col.names=FALSE )
dfinal <- read.table( tempfile )
答案 11 :(得分:17)
请注意,data.table
软件包的tables()
似乎是Dirk的.ls.objects()
自定义函数(在前面的答案中详细介绍)的一个相当不错的替代品,尽管仅适用于data.frames / tables和不是例如矩阵,数组,列表。
答案 12 :(得分:14)
我很幸运,我的大数据集由仪器以大约100 MB(32位二进制)的“块”(子集)保存。因此,我可以在融合数据集之前依次执行预处理步骤(删除无信息部分,下采样)。
手动调用gc ()
“可以在数据大小接近可用内存时提供帮助。
有时,不同的算法需要更少的内存
有时在矢量化和内存使用之间存在折衷
比较:split
&amp; lapply
与for
循环相比。
为了快速和快速简单的数据分析,我经常首先使用一个小的随机子集(sample ()
)的数据。一旦数据分析脚本/ .Rnw完成,数据分析代码和完整数据就会进入计算服务器过夜/周末/ ...计算。
答案 13 :(得分:11)
使用环境代替列表来处理占用大量工作内存的对象集合。
原因:每次修改list
结构的元素时,都会临时复制整个列表。如果列表的存储要求大约是可用工作内存的一半,则会出现问题,因为这样数据必须交换到慢速硬盘。另一方面,环境不受此行为的影响,可以将它们视为与列表类似。
以下是一个例子:
get.data <- function(x)
{
# get some data based on x
return(paste("data from",x))
}
collect.data <- function(i,x,env)
{
# get some data
data <- get.data(x[[i]])
# store data into environment
element.name <- paste("V",i,sep="")
env[[element.name]] <- data
return(NULL)
}
better.list <- new.env()
filenames <- c("file1","file2","file3")
lapply(seq_along(filenames),collect.data,x=filenames,env=better.list)
# read/write access
print(better.list[["V1"]])
better.list[["V2"]] <- "testdata"
# number of list elements
length(ls(better.list))
与big.matrix
或data.table
等结构相结合,允许就地更改其内容,可以实现非常高效的内存使用。
答案 14 :(得分:7)
ll
包中的gData
函数也可以显示每个对象的内存使用情况。
gdata::ll(unit='MB')
答案 15 :(得分:6)
如果您真的想避免泄漏,则应避免在全局环境中创建任何大对象。
我通常做的是拥有一个完成工作并返回NULL
的函数 - 所有数据都在这个函数或其调用的函数中被读取和操作。
答案 16 :(得分:6)
只有4GB的RAM(运行Windows 10,所以大约2或更多,实际上是1GB)我必须非常小心分配。
我几乎只使用data.table。
&#39; fread&#39;函数允许您在导入时按字段名称对信息进行子集化;仅导入实际需要的字段。如果您正在使用基本R读取,请在导入后立即将虚假列归零。
正如 42 - 建议的那样,在可能的情况下,我会在导入信息后立即在列中进行子集化。
我经常在不再需要环境中使用rm()对象,例如:在使用它们对其他内容进行子集之后的下一行,并调用gc()。
&#39;的fread&#39;并且&#39; fwrite&#39;与基本R读写相比,from data.table可以快速非常。
正如 kpierce8 所暗示的那样,我几乎总是把所有东西都写出来并将其重新传入,即使有数千/数十万个小文件也可以通过。这不仅可以保持环境清洁,而且可以保持环境清洁。并保持较低的内存分配,但可能由于严重缺乏可用RAM,R有可能经常在我的计算机上崩溃;真的经常。随着代码在不同阶段的进展,将信息备份到驱动器本身意味着如果崩溃,我不必从头开始。
截至2017年,我认为最快的SSD通过M2端口每秒运行几GB。我有一个非常基本的50GB金士顿V300(550MB / s)SSD,我用它作为我的主磁盘(上面有Windows和R)。我将所有批量信息保存在便宜的500GB WD盘片上。当我开始处理它时,我将数据集移动到SSD。这一点,与“fread”和“fwrite”相结合,一切都很顺利。我尝试过使用过&fff&#39;但更喜欢前者。 4K读/写速度可能会产生问题;从SSD到碟片备份25万个1k文件(价值250MB)可能需要数小时。据我所知,目前还没有任何R套餐可以自动优化&#39; chunkification&#39;处理;例如看一下用户有多少RAM,测试RAM /所有连接的驱动器的读/写速度,然后建议一个最佳的块化&#39;协议。这可以产生一些重要的工作流程改进/资源优化;例如把它分成...... MB用于ram - &gt;将它拆分为...... MB用于SSD - &gt;把它分成......盘子上的MB - &gt;将它拆分为...磁带上的MB。它可以预先对数据集进行采样,从而为其提供更实际的标尺。
我在R中遇到的许多问题涉及形成组合和排列对,三元组等,这使得RAM的限制更多,因为它们通常至少在某些时候呈指数级扩展。这让我把注意力集中在质量上,而不是数量开始时进入它们的信息,而不是试图在之后进行清理,以及准备信息开始的操作顺序(从最简单的操作开始,增加复杂性);例如子集,然后合并/连接,然后形成组合/排列等。
在某些情况下,使用基本R读写似乎有一些好处。例如,在&#39; fread&#39;内进行错误检测。非常好,试图将非常混乱的信息写入R以开始清理它可能很困难。如果您正在使用Linux,Base R似乎也会轻松得多。 Base R似乎在Linux中运行良好,Windows 10使用~20GB的磁盘空间而Ubuntu只需要几GB,Ubuntu所需的RAM略低。但是在(L)Ubuntu中安装第三方软件包时,我注意到了大量的警告和错误。我不建议离开(L)Ubuntu或Linux上的其他股票发行版离得太远,因为你可以放松这么多的整体兼容性,这使得这个过程几乎毫无意义(我认为&#39;团结&#39;是由于自2017年起在Ubuntu取消)。我意识到这对于一些Linux用户来说并不顺利,但是一些自定义发行版的界限毫无意义(我已经花了数年时间单独使用Linux)。
希望其中一些可能会帮助其他人。
答案 17 :(得分:5)
这对上面没有任何补充,但是用我喜欢的简单且重点评论的风格编写。它产生一个表格,其中的对象按大小排序,但没有上面示例中给出的一些细节:
#Find the objects
MemoryObjects = ls()
#Create an array
MemoryAssessmentTable=array(NA,dim=c(length(MemoryObjects),2))
#Name the columns
colnames(MemoryAssessmentTable)=c("object","bytes")
#Define the first column as the objects
MemoryAssessmentTable[,1]=MemoryObjects
#Define a function to determine size
MemoryAssessmentFunction=function(x){object.size(get(x))}
#Apply the function to the objects
MemoryAssessmentTable[,2]=t(t(sapply(MemoryAssessmentTable[,1],MemoryAssessmentFunction)))
#Produce a table with the largest objects first
noquote(MemoryAssessmentTable[rev(order(as.numeric(MemoryAssessmentTable[,2]))),])
答案 18 :(得分:3)
如果您正在使用 Linux 并希望使用多个流程,只需要对一个或多个执行读取操作大型对象使用makeForkCluster
而不是makePSOCKcluster
。这也节省了将大对象发送到其他进程的时间。
答案 19 :(得分:2)
我非常感谢上面的一些答案,在@hadley和@Dirk建议关闭R并发出source
并使用命令行后,我想出了一个对我来说非常好的解决方案。我不得不处理数百个质谱,每个占用大约20 Mb的内存,所以我使用了两个R脚本,如下所示:
首先是一个包装器:
#!/usr/bin/Rscript --vanilla --default-packages=utils
for(l in 1:length(fdir)) {
for(k in 1:length(fds)) {
system(paste("Rscript runConsensus.r", l, k))
}
}
使用这个脚本我基本上控制了我的主脚本runConsensus.r
,并为输出写了数据答案。有了这个,每次包装器调用脚本时,似乎重新打开R并释放内存。
希望它有所帮助。
答案 20 :(得分:2)
除了上面的答案中给出的更一般的内存管理技术,我总是尽量减少对象的大小。例如,我使用非常大但非常稀疏的矩阵,换句话说,大多数值为零的矩阵。使用&#39; Matrix&#39; package(大写重要)我能够将平均对象大小从~2GB减少到~200MB,简单如下:
my.matrix <- Matrix(my.matrix)
Matrix包包含的数据格式可以像常规矩阵一样使用(无需更改其他代码),但能够更有效地存储稀疏数据,无论是加载到内存还是保存到磁盘。
此外,我收到的原始文件是“长”的。格式,其中每个数据点都有变量x, y, z, i
。将数据转换为仅x * y * z
变量i
维数组的效率要高得多。
了解您的数据并使用一些常识。
答案 21 :(得分:2)
这是对这个优秀老问题的新答案。来自哈德利的高级R:
install.packages("pryr")
library(pryr)
object_size(1:10)
## 88 B
object_size(mean)
## 832 B
object_size(mtcars)
## 6.74 kB
答案 22 :(得分:2)
处理需要大量中间计算的对象的提示:当使用需要大量繁重计算和中间步骤来创建的对象时,我经常发现用函数编写代码块很有用创建对象,然后是单独的代码块,使我可以选择将对象生成并保存为rmd
文件,或从我先前已经保存过的rmd
文件外部加载。使用以下代码块结构在R Markdown
中尤其容易做到这一点。
```{r Create OBJECT}
COMPLICATED.FUNCTION <- function(...) { Do heavy calculations needing lots of memory;
Output OBJECT; }
```
```{r Generate or load OBJECT}
LOAD <- TRUE;
#NOTE: Set LOAD to TRUE if you want to load saved file
#NOTE: Set LOAD to FALSE if you want to generate and save
if(LOAD == TRUE) { OBJECT <- readRDS(file = 'MySavedObject.rds'); } else
{ OBJECT <- COMPLICATED.FUNCTION(x, y, z);
saveRDS(file = 'MySavedObject.rds', object = OBJECT); }
```
有了这种代码结构,我要做的就是根据我要生成并保存对象还是直接从现有的保存文件中加载来更改LOAD
。 (当然,我必须先生成并保存它,但是在此之后,我可以选择加载它。)设置LOAD = TRUE
绕过了我复杂函数的使用,并避免了其中所有繁重的计算。此方法仍然需要足够的内存来存储感兴趣的对象,但是它使您不必在每次运行代码时都进行计算。对于需要大量大量计算中间步骤的对象(例如,对于涉及大型数组上的循环的计算),这可以节省大量时间和计算量。
答案 23 :(得分:1)
你也可以使用knitr获得一些好处并将你的脚本放在Rmd chuncks中。
我通常将代码划分为不同的块,并选择哪一个将检查点保存到缓存或RDS文件,
在那里你可以设置一个块保存到&#34;缓存&#34;,或者你可以决定是否运行特定的块。这样,在第一次运行中,您只能处理&#34;第1部分&#34;,您只能选择另一个执行&#34;第2部分&#34;等等。
示例:
part1
```{r corpus, warning=FALSE, cache=TRUE, message=FALSE, eval=TRUE}
corpusTw <- corpus(twitter) # build the corpus
```
part2
```{r trigrams, warning=FALSE, cache=TRUE, message=FALSE, eval=FALSE}
dfmTw <- dfm(corpusTw, verbose=TRUE, removeTwitter=TRUE, ngrams=3)
```
作为副作用,这也可以在再现性方面为您节省一些麻烦:)
答案 24 :(得分:1)
根据@ Dirk&@ Tony的回答,我做了一个小小的更新。结果是在漂亮的大小值之前输出[1]
,所以我拿出了解决问题的capture.output
:
.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, utils::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", "Rows", "Columns")
if (!missing(order.by))
out <- out[order(out[[order.by]], decreasing=decreasing), ]
if (head)
out <- head(out, n)
return(out)
}
# shorthand
lsos <- function(..., n=10) {
.ls.objects(..., order.by="Size", decreasing=TRUE, head=TRUE, n=n)
}
lsos()
答案 25 :(得分:0)
运行
for (i in 1:10)
gc(reset = T)
还会不时地帮助R释放未使用但仍未释放的内存。
答案 26 :(得分:0)
在具有较大中间步骤的大型项目中,我尝试使对象数量保持较小。因此,与其创建许多独特的对象
dataframe
-> step1
-> step2
-> step3
-> result
raster
-> multipliedRast
-> meanRastF
-> sqrtRast
-> resultRast
我使用称为temp
的临时对象。
dataframe
-> temp
-> temp
-> temp
-> result
这给我留下了更少的中间文件和更多的概述。
raster <- raster('file.tif')
temp <- raster * 10
temp <- mean(temp)
resultRast <- sqrt(temp)
要节省更多内存,我可以在不再需要时删除temp
。
rm(temp)
如果我需要多个中间文件,请使用temp1
,temp2
,temp3
。
为了进行测试,我使用了test
,test2
,...
答案 27 :(得分:-1)
rm(list=ls())
是让您保持诚实并保持可重复性的好方法。