R语言使用并行计算提高循环效率
在常规的科研计算程序中,循环几乎是必不可少的结构。处理轻量数据时,for循环方便和迅速,但当计算复杂度较高时,在单线程逐一遍历的for循环常常需要耗费大量时间。
并行计算可以充分发挥计算机的多核心与多线程特性,让循环更高效地完成。最常见的两种并行方式是(1)parallel包和(2)doparallel包与foreach包的组合。
本文使用一个例子演示两种并行的使用方法以及它们与for循环的效率差异。
一、测试内容
使用以下代码创建一个函数test_fun(x)
,作用是内部循环1,000,000次,每次创建一个包含x个1的向量,并对这个向量依次乘以100,除以100,加上100和减去100。
同时定义函数运行次数test_n = 20
。
test_fun <- function(x){
for(i in 1:1000000){
v = rep(1,x)
v = v * 100 / 100 + 100 - 100
}
}
test_n = 20
二、for循环
第一种方法,直接使用for函数循环test_n次test_fun()
,并输出耗费时间。
> start <- Sys.time()
> for(i in 1:test_n){
+ test_fun(100)
+ }
> end <- Sys.time()
> print(end - start)
Time difference of 22.32324 secs
三、parallel包
1. 加载并行包
parallel是R自带的包,不需要安装。
library(parallel)
2. 装载线程
线程并非越多越好,当一次循环所占用的内存较高时,需要保证多个线程同时运行不会让系统内存溢出,否则会出现不可预料的结果。
并行时对于内存中的对象会拷贝多份,在makeCluster()
函数中设置type="FORK"
,可以让多个线程共享内存,降低内存占用。
cl_num <- detectCores() # 检测本机的可用线程数量
cl <- makeCluster(cl_num) # 设置需要启动的线程数量
3. 传入变量和包
clusterExport()和clusterEvalQ()分别用于传入变量和包。本例没有额外的变量和包,因此不需要传入。
clusterExport(cl, c("变量1", "变量2", "函数1"))
clusterEvalQ(cl, library(包名))
4. 并行计算
主要的函数有parApply(X, MARGIN, FUN, …)
,parLapply(cl, X, FUN, …)
和parSapply(cl, X, FUN, ...)
,作用与apply族的apply()
,lapply()
和sapply()
相对应。
本例子不需要任何输出,选择了parSapply()
。cl为创建好的线程组,X为循环的变量,FUN为函数名。
parallel包循环的弊端是只能进行单变量循环。
result <- parSapply(cl = cl,
X = rep(100,test_n),
FUN = test_fun)
5. 释放线程
结束后必须释放线程,否则占用状态会造成资源浪费和其他错误。
stopCluster(cl)
最后完整运行一次程序观察效率的提高效果。
> library(parallel)
>
> cl_num <- detectCores()
> cl <- makeCluster(cl_num)
>
> # clusterExport(cl, c("f10"))
> # clusterEvalQ(cl, library(dplyr))
>
> start <- Sys.time()
> result <- parSapply(cl = cl,
+ X = rep(100,test_n),
+ FUN = test_fun)
> end <- Sys.time()
> print(end - start)
Time difference of 8.583297 secs
四、doparallel+foreach包
这种方法的运作模式除启动函数外,与parallel包是相同的。
foreach()
函数用于循环变量:使用自定义的变量名定义需要循环的内容,可以同时循环多个变量;.export
属性用于传入变量,.packages
属性用于传入包;%dopar%
将变量传给循环函数test_fun(x)
。
result <- foreach(x = rep(100,test_n),
.export = NULL,
.packages = NULL) %dopar% test_fun(x)
完整运行查看计算花费的时间。
> library(doParallel)
> library(foreach)
>
> cl_num <- detectCores()
> cl <- makeCluster(cl_num)
>
> start <- Sys.time()
> result <- foreach(x = rep(100,test_n,),
+ .export = NULL,
+ .packages = NULL) %dopar% test_fun(x)
> end <- Sys.time()
> print(end - start)
Time difference of 9.902606 secs
五、总结
从多次测试的结果来看,计算效率parallel包 > doparallel+foreach包 > for循环。但效率并不会因线程数量增加而成倍地提高。