R语言性能Tips和GC
概述
最近团队在使用R语言作为算法的实践语言,通过人工策略和xgboost算法进行一些价格算法的控制和输出,发现一些代码中对于内存、CPU、程序设计思想以及现代统计算法并不是很熟悉,于是特写此篇普及一下知识,也算是我对R语言的入门文章吧。
GC
对R的内存管理的充分理解将帮助您预测给定任务需要多少内存,并帮助您充分利用您拥有的内存。它甚至可以帮助您编写更快的代码,因为copy造成的副本是代码速度慢的主要原因。希望博主的这篇博客可以帮助您理解R中的内存管理基础知识,从单个对象到函数,再到更大的代码块。 何为GC(garbage collection)?说白了,它是一种机制,确切的说:一种”垃圾”回收机制算法,垃圾是指无用或者长时间占用内存空间的垃圾对象(变量、函数或者类类型实例)。可以将计算机的内存粗略的分为:全局数据区、代码区、栈区和堆区。栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等,但是R不会自动释放function内的临时变量的。堆区:动态内存申请与释放,按需驻留在内存区域,不用时需要释放掉,如不释放掉则会存在内存溢出、地址混淆等各种问题。
test<-function(){ //向量存储在栈内存 x<-c(1:10) } //或者你也可以通过s3或者s4创建class setClass("Person", representation(name = "character", age = "numeric")) //new创建的实例存储在堆内存 hadley <- new("Person", name = "Hadley", age = 31)
gc()
Vcells:向量,Ncells:其它所有,其中gc triger很重要感兴趣的读者可以了解一下。如下图所示:
值得关注的是:R语言用的垃圾回收算法是分代算法,通过一些小技巧name属性来实现copy-on-write(是不是突然想到了Docker分层的copy),因为是分代回收,所以函数里的临时变量都不会马上删掉, 而且每次重复赋值, 上一次的数据依然存在于内存。函数或者什么东西创建的临时变量被释放后,R不会马上调用内存回收gc()函数,所以有时候看windows的任务管理器/Linux的top不能看出R内存变化。R会在内存不够用(要去读C代码)时自动调用gc释放内存。这一点和JAVA类似。这一点和编译语言C/Cpp有非常大的区别,后者要用户手动free或者析构(~Class())。如果不断操作一个占用内存很大的object, 会占用非常多的内存, 所以我们需要把不用的内存gc()释放掉。1.当name为0时, 没有任何object使用它,可以删掉.2.当name为1时, 正在有表达式在用它,所以复制了一份。3.当name为2时, 证明有另一个变量指向了它,当修改时要复制一份出来。我在学习R的GC机制中,看到某网友的封装了R-release function,例如:
r_release <- function(var){ environment() print(tracemem(var)) # unlockBinding("var", .BaseNamespaceEnv) #rm(var) print(eval(var)) print(class(eval(var))) print(sprintf("%s", var)) rm(list = eval(var), envir=parent.frame()) gc(verbose = FALSE) } a<-c(1:1000) r_release("a") 输出为:Error: object 'a' not found
如何做
1.对于自己创建object时,分析清楚数据是不是经常使用常驻内存还是临时object。 2.对于object按值传递还是按引用传递分析清楚,并深入理解R的浅拷贝还是深拷贝。 3.利用R的一些性能和内存分析工具(lineprof、time和memory.profile()),对自己的代码进行分析和探索。 4.将业务和问题域的代码学会使用算法,不仅是机器学习算法还是传统的算法,将时间复杂度和空间复杂度降到最低。 5.能上Rcpp就Rcpp,对C要有信心,语言就是一种工具;学会使用MPI克服多进程的管理。去CRAN上寻找更快的包,例如:fastcluster,princomp,fastmatch,RcppEigen,data.table,dplyr。另外两点也很重要:利用compiler进行提前预编译,进而加快运行速度。在一个就是使用GPU让R运行的更快。6.养成良好的编程习惯(代码风格、注释、设计模式和深度思考的习惯即问题本质)。 暂时想到这些
实践
1.能用向量化就要用向量化(即矩阵),进而转化成线性代数求解和并行加速。
利用内置的向量化函数,比如exp、sin、rowMeans、rowSums、colSums、ifelse等
利用Vectorize函数将非向量化的函数改装为向量化的函数
函数族:apply、lapply、sapply、tapply、mapply等
plyr和dplyr包
Rstudio发布的data wrangling cheat sheet
n <- 100000 x1 <- 1:n x2 <- 1:n y <- vector() system.time( for(i in 1:n){y[i] <- x1[i] + x2[i]} ) #for time输出 user system elapsed 0.032 0.005 0.037 system.time(y <- x1 + x2) #向量化时间占用 user system elapsed 0.000 0.000 0.001
:执行调用进程的用户态指令所占用的CPU时间。system time:内核态系统调用进程执行的CPU时间。elapsed time:约等于用户态+内核态时间我们能看到采用for循环时间是向量化矩阵时间37倍,并且在用户态和内核态的时间基本上是没有时间消耗。所以利用R内置的向量化函数,自定义向量化函数,只要在函数定义时每个运算是向量化的。(利用rowMeans、rowSums、colSums、colMeans等函数对矩阵或数据库做整体处理)。如果我们在函数定义时加了逻辑判断表达式会破坏向量化计算的。
test <- function(x){ if(x %% 3 == 0){ result <- TRUE }else{ result <- FALSE} return(result) } test(12) test(c(1:10)) #输出中会有Warning 警告 Warning message: In if (x%%3 == 0) { : the condition has length > 1 and only the first element will be used
所以在学会利用函数ifelse、Vectorize和sapply转化向量化运算。
# 利用ifelse函数做向量化的判断 func <- function(x){ ifelse(x %% 3 == 0,TRUE,FALSE) } func(c(1,2,3,4)) # 利用Vectorize函数将非向量化的函数改装为向量化的函数 funcv <- Vectorize(func) funcv(c(1,2,3,4))
2.R是一门解释性动态语言,在运算过程会动态分配内存,提高灵活性,但降低了效率。所以要尽量避免使用bind技巧和内存copy,预先给对象分配内存。
## 求出10000个斐波那契数 x <- c(1,1)i <- 2system.time( while(i<10000){ new <- x[i] + x[i-1] x <- cbind(x,new) i <- i + 1 })## 指定类型和长度 x <- vector(mode="numeric",100000)x[1] <- 1x[2] <- 1system.time( while(i<10000){ i <- i + 1 x[i] <- x[i-1] + x[i-2] })
在使用bind和不使用bind的效果,计算结果如下图对比看出,后者是前者时间性能100倍。
我们再看一个例子是关于避免内存copy的问题,#假设我们有许多彼此不相关的向量,但因为一些其他的原因,我们希望将每个向量的第四个元素设为12。
m <- 5000 n <- 1000 z <- list() for(i in 1:m) z[[i]] <- sample(1:10, n, replace = T) system.time(for(i in 1:m) z[[i]][4] <- 12) #输出 user system elapsed 0.051 0.011 0.061 # 把这些向量一起放到矩阵中 z <- matrix(sample(1:10, m * n, replace = T),nrow = m) system.time(z[,4] <- 12) #输出 user system elapsed 0.014 0.010 0.023
3.删除临时对象和不再用的对象
rm()删除对象 rm(object)删除指定对象,rm(list = ls())可以删除内存中的所有对象
gc()内存垃圾回收 使用rm(object)删除变量,要使用gc()做垃圾回收,否则内存是不会自动释放的。invisible(gc())不显示垃圾回收的结果
4.经常使用分析内存的函数
object.size()返回R对象的大小
memory.profile()分析cons单元的使用情况
5.学会使用并行计算和分布式计算接口
并行计算后端包有如下:
doMPI与Rmpi包配合使用
doRedis与rredis包配合使用
doMC提供parallel包的多核计算接口
doSNOW提供现已废弃的SNOW包的接口
下面介绍一下CUDA和R如何搞事情,呵呵。
本来想写一下R+GPU、R+CPP、R+MPI,时间有限以后再向读者介绍。
gc和rm区别
gc不会删除你仍在使用的任何变量,它只释放不再有权访问的内存,运行gc()永远不会让你失去变量。—这很重要
- 点赞
- 收藏
- 关注作者
评论(0)