概述
最近团队在使用R语言作为算法的实践语言,通过人工策略和xgboost算法进行一些价格算法的控制和输出,发现一些代码中对于内存、CPU、程序设计思想以及现代统计算法并不是很熟悉,于是特写此篇普及一下知识,也算是我对R语言的入门文章吧。
GC
对R的内存管理的充分理解将帮助您预测给定任务需要多少内存,并帮助您充分利用您拥有的内存。它甚至可以帮助您编写更快的代码,因为copy造成的副本是代码速度慢的主要原因。希望博主的这篇博客可以帮助您理解R中的内存管理基础知识,从单个对象到函数,再到更大的代码块。 何为GC(garbage collection)?说白了,它是一种机制,确切的说:一种”垃圾”回收机制算法,垃圾是指无用或者长时间占用内存空间的垃圾对象(变量、函数或者类类型实例)。可以将计算机的内存粗略的分为:全局数据区、代码区、栈区和堆区。栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等,但是R不会自动释放function内的临时变量的。堆区:动态内存申请与释放,按需驻留在内存区域,不用时需要释放掉,如不释放掉则会存在内存溢出、地址混淆等各种问题。比如C 和C等语言是需要程序员手动释放堆区内存,但是Python、R等都有自己实现了内存回收机制,让coder专注于自己的业务和问题域,但这不代表你可以不关心,这是大错特错。例如:
代码语言:javascript复制test<-function(){
//向量存储在栈内存
x<-c(1:10)
}
//或者你也可以通过s3或者s4创建class
setClass("Person", representation(name = "character", age = "numeric"))
//new创建的实例存储在堆内存
hadley <- new("Person", name = "Hadley", age = 31)
代码语言:javascript复制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,例如:
代码语言:javascript复制 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
user time:执行调用进程的用户态指令所占用的CPU时间。 system time:内核态系统调用进程执行的CPU时间。 elapsed time:约等于用户态 内核态时间 我们能看到采用for循环时间是向量化矩阵时间37倍,并且在用户态和内核态的时间基本上是没有时间消耗。所以利用R内置的向量化函数,自定义向量化函数,只要在函数定义时每个运算是向量化的。(利用rowMeans、rowSums、colSums、colMeans等函数对矩阵或数据库做整体处理)。如果我们在函数定义时加了逻辑判断表达式会破坏向量化计算的。
代码语言:javascript复制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转化向量化运算。
代码语言:javascript复制# 利用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,预先给对象分配内存。
代码语言:javascript复制## 求出10000个斐波那契数
x <- c(1,1)
i <- 2
system.time(
while(i<10000){
new <- x[i] x[i-1]
x <- cbind(x,new)
i <- i 1
}
)
## 指定类型和长度
x <- vector(mode="numeric",100000)
x[1] <- 1
x[2] <- 1
system.time(
while(i<10000){
i <- i 1
x[i] <- x[i-1] x[i-2]
}
)
在使用bind和不使用bind的效果,计算结果如下图对比看出,后者是前者时间性能100倍。
我们再看一个例子是关于避免内存copy的问题,#假设我们有许多彼此不相关的向量,但因为一些其他的原因,我们希望将每个向量的第四个元素设为12。
代码语言:javascript复制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()永远不会让你失去变量。—这很重要
算法总结
R是一门解释性动态语言,知道这一点对于你在采用计算机科学 统计学思想编程时会得心应手。根据你的业务问题,采用统计学算法包、面向函数编程或者面向对象编程都是很简单,因为R包装了很多统计学包,无须关注底层思想和实现,可以说是开箱即用。但是想站在数据科学和算法层面分析问题时,必须深入理解算法底层和设计思想,这样你才能事半功倍。一个优秀的算法专家或数据科学家,首先是一名合格的工程师。算法层面一定对高级抽样技术、高级贝叶斯分析、统计学习方法、现代分层分位回归、复杂数据的统计推断以及统计预测区间(双参数指数分布预测区间、韦布尔分布与极值分布预测区间、正太分布预测区间和幂律过程的预测区间)等知识深入理解,并不断实践和应用理论算法解决问题,才会让自己的内功不断强大。 路漫漫其修远兮,吾将上下而求索
参考文献
1.R语言垃圾回收机制 2.R memory 3.ParallelR