R语言性能Tips和GC

2018-08-08 10:38:28 浏览数 (1)


概述

最近团队在使用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
代码语言:javascript复制
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

0 人点赞