最近在写R代码的时候遇到一个问题,用一个新变量覆盖一个就变了会不会节省内存,比如如下对一个大向量加1:

aa <- rnorm(100000)
方法一:
bb <- aa + 1
方法二:
aa <- aa + 1

方法二是不是更加节省内存?

于是在某大佬群里问了这个问题,但是没有人回答。于是,索性在网上找答案,顺便全面了解了一下如何写出高效率的R程序。

本文主要讨论如何提高R语言运行效率。资料来源于《Efficient R programming》,结合本人实践,对其中的内容作了概要,提取了有用的部分。之前也多次介绍个如何提高R语言运行速度,本文系统全面地对这一问题进行了讨论。

在开始介绍之前,先来认识一下“microbenchmark”包,看看它如何比较不同程序运行时间的。首先我们比较3种访问数据框中某个单元格值的速度。如下:

# install.packages("microbenchmark")
library(microbenchmark)
df <- data.frame(v=1:4, name = letters[1:4])
microbenchmark(df[3,2], df[3,"name"], df$name[3])
## Unit: microseconds
##           expr    min      lq     mean  median      uq     max neval cld
##       df[3, 2] 12.328 13.8680 22.52158 16.1860 22.5270 193.961   100   b
##  df[3, "name"] 12.294 13.3890 20.85873 16.9320 24.2475  73.178   100   b
##     df$name[3]  3.845  4.3685  6.20709  4.6765  6.6070  34.672   100  a

通过对三种写法的比较可以看出来,df$name[3]这种写法运行速度要快很多,所以我们平时访问数据框某个值时,可以多使用df$name[3]形式,尽量避免df[3,2]和df[3,"name"]的形式。

进入正题 ==>

高效的代码

  • 减少函数调用
  • 避免逐步扩增向量
  • 尽可能向量化
  • 避免对变量的重复计算

减少函数调用

和C/C++等语言强调数据类型不同,R语言为用户隐藏了数据类型,这为R用户带来了方便,但是付出的代价就是运行效率。

R语言的很多基础函数也是用C语言写的,你可以认为R函数是对C语言函数的包装,方便用户的调用。比如R中的随机数函数runif():

runif
## function (n, min = 0, max = 1)
## .Call(C_runif, n, min, max)
## <bytecode: 0x7f9a26a69200>
## <environment: namespace:stats>

实际上是调用了C语言编写的C_runif函数。这在一定程度上提高了R的运行效率。如何在R中编写并调用C函数呢?请参考:

在R语言中写一个C函数

提高R运行效率的另一个策略是尽可能少的调用函数。比如对一个x向量中的每一个数加1

方法1:

x <- 1:10
x <- x + 1

方法2:

x <- 1:10
for(i in seq_len(10)){
    x[i] <- x[i] + 1
}

第二种方法显然比第一种方法效率低。这并不只是因为第二种方法使用了for循环,而且第二种方法调用了很多次函数。

你可能要问,第二种方法不就是调用了for和seq_len两个函数吗?

其实不是,并不是只有函数名的才叫函数,运算符也是函数,比如这里的+, [ ,<-等都是在调用函数!比如2+3你还可以写成"+(2,3)"。

所以,提高R运行效率要尽量少的调用函数(包括运算符)。

内存分配

对于一个列表、数据框或者向量,要提前分配内存空间,避免计算过程中扩增内存。扩增内存涉及到内存的申请和数据的拷贝,会耗费大量的时间。【不仅适用于R语言,对C++也同样适用】。

下面我们使用3种方法来创建一个向量。

方法1:逐步扩增向量

method1 = function(n) {
  vec = NULL # Or vec = c()
  for(i in seq_len(n))
    vec = c(vec, i)
  vec
}

方法2:提前分配内存空间

method2 = function(n) {
  vec = numeric(n)
  for(i in seq_len(n))
    vec[i] = i
  vec
}

方法3:使用内置函数

method3 = function(n) seq_len(n)

下面比较3种方法

microbenchmark(times = 50, unit = "ms",
               method1(1e4), method2(1e4), method3(1e4))
## Unit: milliseconds
##            expr        min         lq         mean      median         uq
##  method1(10000) 129.468015 140.285307 153.22915268 145.5655380 150.856140
##  method2(10000)   0.550268   0.587439   0.84521070   0.6021805   0.615770
##  method3(10000)   0.000255   0.000505   0.03704442   0.0025455   0.006321
##         max neval cld
##  235.067523    50   b
##    7.749376    50  a
##    1.711064    50  a

通过比较可以看到,第二种方法(0.8ms)比第一种方法(153ms)快了数百倍!当然,最快的还是内置函数(0.037ms),因为它调用了C函数,几乎瞬间完成。

所以,提前给向量等分配内存空间,避免拷贝扩增!!

向量化

什么是向量化,比如对100个随机数中的每一个都加1,可以用:

x <- runif(100) + 1

这儿没有使用for循环对其中的每一个数加1,而是直接对全部值都加1。这就是向量化。

这一点尤为重要,尤其是在对大矩阵进行计算的时候,向量化能够节省成百上千倍的时间。

举例:使用蒙特卡洛模拟在0-1之间的积分值。

如果我们不使向量化计算:

monte_carlo = function(N) {
  hits = 0
  for (i in seq_len(N)) {
    u1 = runif(1)
    u2 = runif(1)
    if (u1^2 > u2)
      hits = hits + 1
  }
  return(hits / N)
}

向量化上述过程:

mente_carlo_vec <- function(N){
    ret = sum(runif(N)^2 > runif(N))/N
    return(ret)
}

比较上述两种方法:

microbenchmark(times=50, unit="ms",
               monte_carlo(1e4), mente_carlo_vec(1e4))
## Unit: milliseconds
##                    expr       min        lq      mean     median        uq
##      monte_carlo(10000) 29.396726 32.819228 35.424517 34.6538550 38.820023
##  mente_carlo_vec(10000)  0.531643  0.537847  0.629842  0.5471445  0.619557
##        max neval cld
##  42.142099    50   b
##   3.030584    50  a

向量化的过程(0.63ms)比非向量化(35.4ms)的过程快了数十倍!

apply族函数

常见的apply族函数有如下一些:

个人使用习惯不同,本人最常使用的是apply和lapply。apply族函数具体用法不在此处做详细介绍。使用apply族函数能够避免使用for循环,不仅提高代码效率,还能提高代码的可读性和美观程度。

缓存变量

合理使用缓存变量,避免重复计算。比如我们对矩阵的每一列进行正态化。我们可以使用:

apply(x, x, function(i) mean(i)/sd(x))

这儿sd(x)重复计算了好多次,所以,我们可以使用缓存变量,避免正态化一列时都要进行一次计算。如下:

sd_x <- sd(x)
apply(x, 2, function(i) mean(i)/sd_x)

对于一个100*1000的矩阵,上述使用缓存变量可以达到上百倍的效率提升。

【左侧standard:多次重复计算变量;右侧cached:使用缓存变量,避免重复计算】

字节编译

很多R基础函数都已经编译成了字节码,这样可以大大提高运行效率。通常我们可以使用compiler包对自定义函数进行预编译。但是自3.4版本之后,R语言可以自动实现对自定义函数的预编译。

高效读写(I/O)

R有很多文件读写函数。对于文本文件的读写,特别是表格文件,我们常用read.csv,readr::read_csv或者data.table::fread。

通过对大文件读入的比较,read_csv和fread要比R本身自带的read.csv要快很多。另外,在读入文件的时候,声明变量类型也能提高读入速度。比如:

read.csv(file_name, colClasses = c("numeric","numeric"))

本人常用fread,使用体验非常好,不光是速度很快,还能能够精确控制要读入的行和列,从而很大程度上减少内存和CPU消耗。

除了文本文件,还有二进制文件,.Rds和.RData是R语言默认的二进制文件形式,这种二进制文件形式提高了文件读写速度和文件的压缩比。本人一个606Mb大小的文本文件保存为二进制后,只有99Mb。所以如果你对文件读写速度和文件大小有特殊需求,可以将文件保存为.Rds的二进制文件。但是,缺点是无法随时查看文件,每次查看都要读入R中。

高效数据清理和重塑

主要是介绍tibble, tidyr和dplyr等包处理数据表的用法,以及stringr等处理字符串的用法。

dplyr包是比较好用的数据处理包,其中很多函数都是还是用C++写的,运行效率很高。更关键的是,它能够很好使用管道符%>%,这简直是处理数据的神器,使得R代码变得十分整洁高效。

另一个很高效的数据清理汇总包是data.table,很多时候它比dplyr包还要快,而且有很多特殊方便的使用方法。比如,在data.table中你可以设置关键词,能够进一步提高数据处理效率。

【不同大小数据集切片相对速度比较,设置关键词的data.table(DT:key)要比其他几种方式快很多】

高效优化

  • 在优化之前,要弄清楚哪儿是代码运行的瓶颈
  • 如果你的数据表或者数据框是同一种数据形式,那么最好把它转化为矩阵。
  1. 使用并行运行策略parallel
  • 对部分低效代码使用C++重写 如果你很清楚代码运行慢的一部分,那么着重对这一部分进行优化即可。但是,如果你的代码很长,运行慢的地方又不是很明显,那么可以使用profvis等包来显示各部分代码的运行时间。

你可能会认为ifelse应该比if快,但实际上,并非如此。ifelse虽然写法上更加优雅,但是运行效率并不比if高。

apply族函数可以对行或列进行操作,不过对行列操作效率最高的是rowSums,colSums,rowMeans和colMeans等。所以,其行列的加和或者均值的时候,尽量用这些函数。

矩阵的计算要比数据框快很多,如果你的数据是同一种数据形式,尽量转换为矩阵。

能用整数的时候尽量用整数,不要使用实数。在R中L表示整数,比如6L。比如查看一个向量的前6个元素:

x <- runif(10)
head(x,6.0)
head(x,6L)

使用6L效率会有微小提升。

使用并行计算,充分利用多核CPU能够较大程度的提高运行效率。parallel包配合apply族函数,能够很简单的实现并行计算。如果是在Linux或OS X系统上,可以使用mclapply进行并行计算【本人觉得这函数非常好用】。

最后,提高运行效率的最大杀手锏是Rcpp,即R语言的C++接口。相比于C语言接口,Rcpp的使用非常友好方便。特别是在处理循环问题上,Rcpp的效率优势发挥到了极致。

Rcpp的使用“罄竹难书”,此处不做详细介绍,可以参考本公众号之前的文章。

高效硬件配置

显然硬件配置越高,程序运行遇到的瓶颈相对越小。更大的RAM,更快的SSD盘取代HDD硬盘;更高主频的CPU等等。

更高效的协同工作

养成良好的代码写作习惯,比如代码块以及大括号的使用,良好的文件命名规范,R代码通常.R为文件后缀,而不是.r。

加载包时,通常是library("dplyr"),而不是library(dplyr)。

养成写代码注释的习惯,让别人能够很快的读懂你的代码,同时也方便自己以后查询。【程序员最讨厌做两件事:阅读别人没有写注释的代码;为自己的代码写注释】

养成函数和变量的良好命名规范,做到“见名知意”。

命名最好使用_来代替.,使用.可能会造成歧义。

使用TRUE/FALSE来代替T/F。

赋值时,使用<-来代替=。

变量和操作符之间使用单个空格隔开,x <- x + 1,而不是x<-x+1。

使用git等工具进行版本控制

===> END <===

最后,再来回答开头的那个问题。用一个变量覆盖另一个变量确实能够大大节省内存!你可以使用“lobstr”包来查看内存变化,你会发现第二种方法几乎没有占用额外的内存。

【谢谢阅读,欢迎转发分享】

资料来源:https://csgillespie.github.io/efficientR/