R并行计算(parallel computing)-1-使用foreach函数

CPU核数和线程数的含义

处理器的核心数一般指的是物理核心数,又称为内核。双核就是包括2个独立的CPU核心单元组,四核就是包含4个独立的CPU核心单元组,是处理各种数据的中心计算单元。

一般一个核心对应一个线程,而intel开发出了超线程技术,1个核心能够做到2个线程计算,而7个核心则能够做到12个线程。线程数是一种逻辑的概念,是虚拟出的CPU核心数。

举个例子,CPU可以想象成是一个银行,CPU核心就相当于柜员,而线程数就相当于开通了几个窗口,柜员和窗口越多,那么同时办理的业务就越多,速度就越快。

通常情况下,一个柜员对应的是一个窗口,通过超线程技术相当于一个柜员管理着两个窗口,使用左右手同时办理两个窗口的业务,大大提高了核心的使用效率,增加了办理业务的速度。

R为什么需要使用并行计算?

从内存角度来看,R采用的是内存计算模型(In-Memory),被处理的数据需要预取到贮存(RAM)中。其优点是计算效率高、速度快,但缺点是这样一来能处理的问题规模就非常有限(小于RAM的大小)。

另一方面,R默认只有一个单线工作,让其他线程闲着显然是一种浪费。

例如,如果把R跑到一个具有260核的CPU上,单线程的R程序最多只能利用到1/260的计算能力,从而浪费了其他259/260的计算核心。现在的计算机,都具有4-16核,无论是高配置的计算机还是低配置的计算机,如果不使用并行计算,每台计算机的运行速度相差不大。

并行计算就是让多个线程或者全部线程同时工作。

R并行计算

查看电脑的物理核数线程数

detectCores(logical = F)#查看电脑的物理核数
#18

install.packages("future")
library(future)
availableCores()#查看电脑可用的线程数
#36

例子1

使用for循环,执行重复任务,每次循环中,计算10万个服从标准正态分布的随机数,然后计算其均值,这个过程重复3万次。代码如下:

#example 1
timestart <- Sys.time()
x <- numeric()
for (i in 1:30000) {
  x[i] <- mean(rnorm(1e5))
}
timeend <- Sys.time()
runningtime <- timeend - timestart
print(runningtime)
#3.83mins

在R中尽量避免使用for循环,在R中使用for循环速度很慢,在写代码的时候,应该尽可能的避免使用for循环,可以考虑向量化编程(整体思想,将向量/矩阵进行操作,使得其中的每个元素都进行相同的操作)。

1-foreach最简单的用法(非并行计算)

使用%do%+foreach()代替for循环,每次循环都计算10万个标准正态分布的随机数,共循环3万次。从结果上看,计算速度和for循环差不多。值得注意的是,函数foreach返回了一个列表(list)。使用foreach的一个优势在于,%do%后的花括号{}之间可以像for循环那样写多条语句。

timestart <- Sys.time()
library(foreach)
x2 <- list()
foreach(i = 1:30000) %do% {
  x2[[i]] <- mean(rnorm(1e5))
}
timeend <- Sys.time()
runningtime <- timeend - timestart
print(runningtime)
#3.91 mins

2-foreach并行计算

使用foreach进行并行计算,此时需要将上述的%do%替换为%dopar%来启动并行计算。在使用并行计算之前,首先需要加载doParallel包,创建一个集群并注册。以18个物理核心,36线程的计算机为例。计算前面计算正态分布随机数均值的代码:

仅用约19秒完成同样的任务,比起使用单个核心快了将近10倍!foreach默认的返回值数据类型为list,但是我们更多希望使用向量或者矩阵形式,这时可以使用".combine"参数来指定输出数据的类型。

library(foreach)
library(doParallel)
# 创建一个集群并注册
cl <- makeCluster(18)
registerDoParallel(cl)


# 启动并行计算
time1 <- Sys.time()
x2 <- foreach(i = 1:3e4, .combine = c) %dopar% {
  mean(rnorm(1e5))
}
time2 <- Sys.time()
print(time2-time1)
#19sec in cl <- makeCluster(36)

#21sec in cl <- makeCluster(18)

# 在计算结束后别忘记关闭集群
stopImplicitCluster()
stopCluster(cl)

foreach函数中也可以使用rbind或者cbind等函数以矩阵形式输出结果:

timestart <- Sys.time()
x4 <- matrix(0,nrow=3e4,ncol=6)
for (i in 1:30000) {
  x4[i,] <- summary(rnorm(1e5))
}
timeend <- Sys.time()
runningtime <- timeend - timestart
print(runningtime)
#6.036685 mins


# 创建一个集群并注册
cl <- makeCluster(36)
registerDoParallel(cl)
# 启动并行计算
timestart <- Sys.time()
x <- foreach(i = 1:3e4, .combine = rbind) %dopar% {
  summary(rnorm(1e5))
}
timeend <- Sys.time()
runningtime <- timeend - timestart
print(runningtime)
stopImplicitCluster()
stopCluster(cl)
# 26.86028 secs

foreach函数的一些重要参数

(1).package

写在%dopar%后的代码经常会用到第三方R包,这些包必须在.package中指定,例如机器学习中经常会用到的随机森林算法:

x <- matrix(runif(500), 100)
y <- gl(2, 50)
rf <- foreach(
  ntree = rep(250, 4),.combine = combine,.packages = 'randomForest') %dopar% {
    randomForest(x, y, ntree = ntree)
  }

(2).errorhandling

.errorhandling可以处理循环中出现错误时的应对方法,默认为stop。举个例子,这里故意让i=5时报错,代码如下 :

x <- foreach(i = 1:10, .combine = c,.errorhandling = "stop") %dopar% {
  if(i == 5)
    stop('STOP!')
  i
}
Error in { : task 5 failed - "STOP!"

x
错误: 找不到对象'x'

一个报错就会导致前面4次任务都白跑了,如果已经跑了几小时的任务毁在了一个循环的报错,砸键盘的心情都有了。此时可以修改.errorhandlingremove,跳过报错的循环,例如

x <- foreach(i = 1:10, .combine = c,.errorhandling = "remove") %dopar% {
  if(i == 5)
    stop('STOP!')
  i
}
x
[1]  1  2  3  4  6  7  8  9 10

可以看到,第五次报错的循环被跳过了。当然,我们还是希望找出报错问题所在并尝试解决,此时可以修改.errorhandlingpass:

x <- foreach(i = 1:7, .combine = rbind, .errorhandling = "pass") %dopar% {
  if(i == 5)
    stop('STOP!')
  c(i, i^2)
}
x
         message call      
result.1 1       1         
result.2 2       4         
result.3 3       9         
result.4 4       16        
result.5 "STOP!" Expression
result.6 6       36        
result.7 7       49 

# 第五次循环的报错信息被记录了下来

4、关于环境和变量作用域的一些注意事项

一个R函数不仅有参数(arguments)和主体(body),还有自己的环境(environment),R中环境这个概念可能对大部分用户有些陌生,简单说一下:

x <- 1:3
# 创建一个名为f的函数
f <- function(x){
  k <- 3
  h <- function(){
    x <- sqrt(x)
  }
  print(environment(h))
  return(x + k)
}
# 查看函数f所处的环境
environment(f)
<environment: R_GlobalEnv> 

运行代码environment(f)的结果告诉我们:函数f()是在顶层环境R_GlobalEnv创建的。在函数f()的内部,我们又创建了一个新的函数h()和变量k,在C语言的概念里,变量k是函数f()的局部变量。但在R中,h()k都可以视为f()的局部变量,并且两者对于顶层环境是不可见的。如果运行函数f(),会到的这样的结果:

f(x)
<environment: 0x0000021b918e0310>
[1] 4 5 6

print函数告诉我们h()并不在R_GlobalEnv环境内,而是位于一个名为0x0000021b918e0310的环境内(这其实是一个内存地址)。

如果将代码改成这样运行:

x1 <- 1:3
f <- function(x){
  x2 <- 5
  return(h())
}

h <- function(){
  x1 + x2
}
f(x1)
Error in h() : 找不到对象'x2'

就会报错,这时因为函数h()定义在了顶层环境R_GlobalEnvf()在其自身环境内创建的局部变量x2对于h()是不可见的,这里就是作用域(scope)的概念。

下面说一下foreach作用域的特点,举个例子:

x1 <- 1
x2 <- 2
f <- function(x1) {
  foreach(i = 1:100, .combine = c)  %dopar% {
    x1 + x2 + i
  }
}
f(x1) 
Error in { : task 1 failed - "找不到对象'x2'" 

如上所示,运行函数f()会报错。

原因在于,如果你将foreach写在了一个函数内部,那么foreach运行时会导出(export)它所在环境(也就是函数f()的环境)内所有必要变量,但却不会主动加载上一级环境(这里是R_GlobalEnv)中的变量。在这段代码中,x2并不是函数f()的必要变量,因此并未被放在f()的环境内,导致foreach没有识别到它。面对这种情况有两种解决方法:

(1)将x2写为f()的参数

x1 <- 1
x2 <- 2
f <- function(x1, x2) {
  foreach(i = 1:3, .combine = c)  %dopar% {
    x1 + x2 + i
  }
}
f(x1, x2) 
[1] 4 5 6

(2)使用foreach.export参数:

x1 <- 1
x2 <- 2
f <- function(x1) {
  foreach(i = 1:3, .combine = c, .export = 'x2')  %dopar% {
    x1 + x2 + i
  }
}
f(x1) 
[1] 4 5 6

5、最后的一点碎碎念

(1)时间(计算速度)和空间(内存用量)的权衡:R中的并行计算对于内存消耗大,因此请谨慎选择适当的CPU核心数量(或是从代码上优化内存用量)。如果你要处理的数据很大,内存又不够,电脑就会脱机“卡死”,可行的解决方案如下:

①多插内存条

②把任务分解,例如1W的数据,拆成5个2k

(2)在内存足够的情况下,核心数量并不是越多越好:我在计算一个模型时,统计了从使用1核到36核跑完模型所需时间(2-7列名是数据体积)。当调用的核心数量超过物理核心的1.5倍时(18*1.5=27核),超线程带来的潜力已经几乎被榨干,此时并行计算带来的速度提升已经不大了。

因此,对于支持超线程的CPU,并行计算时建议选择物理核心数量的1.5倍为上限,可以用detectCores(logical = F)命令查看自己电脑的物理核心数量。

(3)并行一时爽,也要留意CPU的散热问题,尤其是笔记本电脑。全核运行会让散热不好的电脑分分钟降频甚至死机。

MM.fun <- function(){
  b <- 1+2
  return(b)
}

cfun <- function(c){
test <- 1:10
m <- length(test)
aa <- MM.fun(m)#全局变量和局部变量的区别,这个时候,m是局部变量,没有办法传递给MM.fun
f <- c+aa
return(f)
}

cfun(1)

参考:

CPU核数和线程数有什么作用?CPU核数和线程数什么意思?CPU核数和线程的关系与区别 - 知乎 (zhihu.com)

Speed boosting in R: Writing efficient code & parallel programming | R-bloggers

【多核的春天】R语言里的并行计算 - 知乎 (zhihu.com)

猜你喜欢

转载自blog.csdn.net/u011375991/article/details/131272023