R的循环效率比较慢,通常可以使用apply族函数进行加速,那么多线程怎么整?只介绍方法,原理不是很懂。
R会给出程序运行时间,供参考:
- 用户:是消耗在应用程序(非操作系统部分)执行的时间
- 系统:是底层操作系统执行(例如磁盘读写等)部分的时间
- 流逝:是经过的总时间(可以认为是前两者的总和)
snow包
先查看电脑有几个core
我现有电脑配置是6核,逻辑处理器是12
parallel::detectCores()
12
# 不设置多线程
vec <- 1:10000000
cat("-------- vapply执行 --------")
system.time(vapply(vec,function(x) x+2,FUN.VALUE=0))
# 开启多进程
cat("-------- Core设置为5 --------")
clus <- snow::makeCluster(5)
system.time(snow::parLapply(clus,vec, function(x){x+2}))
cat("-------- Core设置为10 --------")
clus <- snow::makeCluster(10)
system.time(snow::parLapply(clus,vec, function(x){x+2}))
# 关闭多进程
snow::stopCluster(clus)
-------- vapply执行 --------
user system elapsed
4.50 0.03 4.54
-------- Core设置为5 --------
user system elapsed
2.14 0.44 6.58
-------- Core设置为10 --------
user system elapsed
2.11 0.25 4.99
**可以看到做了多线程之后,user时间变少了。**但是很多时候我们的处理过程并不是简单的向量apply操作,比如做分析时,会有多个SQL需要查询,这些SQL本身独立,也可多线程的操作,该如果处理呢?看个Demo:
- 有一个数据框,两列(name,value)
- 要求做groupby操作进行sum求和,name作为参数多线程计算
构造数据
demoDf <- data.frame(name=sample(letters,100000,replace = TRUE),value=seq(1,100000))
进行groupby的操作
demofunction <- function(name,df=demoDf){
subDf <- subset(df,name==name)
return(data.frame(name=name,sum=sum(subDf$value)))
}
system.time(expr = {
clus <- snow::makeCluster(10)
# 提交多线程用到的变量&函数
snow::clusterExport(clus, c("demofunction", "demoDf"))
res <- snow::parLapply(clus, letters, function(x) demofunction(name = x, df = demoDf))
# 关闭多进程
snow::stopCluster(clus)
resDf <- do.call(rbind, res)
})
user system elapsed
0.01 0.00 1.69
head(resDf,3)
name | sum |
---|---|
<fct> | <dbl> |
a | 5000050000 |
b | 5000050000 |
c | 5000050000 |
foreach和doParallel
snow包的用法像是apply族的并行化,foreach和doParallel更像是对for循环的并行化。还是那句话原理不是很懂,看代码~
library(foreach)
# library(doParallel)
eig <- function(n, p){
x <- matrix(rnorm(100000), ncol=100)
r <- cor(x)
eigen(r)$values
}
n <- 1000000
p <- 100
k <- 500
cat("-------- 不并行 --------")
# 不并行
system.time(
x <- foreach(i=1:k, .combine=rbind) %do% eig(n, p)
)
cat("-------- Core设置为2 --------")
cl<- snow::makeCluster(2) # 注册线程,可以是detectCores()返回的核数。
## 登记内核数量
doParallel::registerDoParallel(cl)
system.time(
y <- foreach(i=1:k, .combine=rbind) %dopar% eig(n, p)
)
snow::stopCluster(cl) # 结束集群
cat("-------- Core设置为5 --------")
cl<- snow::makeCluster(5) # 不过R中能运行满线程 ,即detectCores()返回的核数。
## 登记内核数量
doParallel::registerDoParallel(cl)
system.time(
y <- foreach(i=1:k, .combine=rbind) %dopar% eig(n, p)
)
snow::stopCluster(cl)
cat("-------- Core设置为10 --------")
cl<- snow::makeCluster(10) # 不过R中能运行满线程 ,即detectCores()返回的核数。
## 登记内核数量
doParallel::registerDoParallel(cl)
system.time(
y <- foreach(i=1:k, .combine=rbind) %dopar% eig(n, p)
)
snow::stopCluster(cl)
Warning message:
"package 'foreach' was built under R version 3.6.3"
-------- 不并行 --------
user system elapsed
6.22 0.00 6.21
-------- Core设置为2 --------
user system elapsed
0.14 0.06 3.38
-------- Core设置为5 --------
user system elapsed
0.06 0.02 1.47
-------- Core设置为10 --------
user system elapsed
0.20 0.03 1.04
从系统总耗时看,所耗费时间是降低的。再看一个简单的例子:
system.time(expr = {
foreach(i = 1:4) %do% {
Sys.sleep(5)
i
}
})
user system elapsed
0.00 0.00 20.03
cl<- snow::makeCluster(5) # 不过R中能运行满线程 ,即detectCores()返回的核数。
## 登记内核数量
doParallel::registerDoParallel(cl)
system.time(expr = {
foreach(i = 1:4) %dopar% {
Sys.sleep(5)
i
}
})
snow::stopCluster(cl)
user system elapsed
0.02 0.00 5.03
当不并行时,sleep5s串行了4次,共计20s;并行之后相当于在不同线程同时sleep一次,5s;
snow和foreach
system.time(expr = {
clus <- snow::makeCluster(5)
# 提交多线程用到的变量&函数
snow::clusterExport(clus, c("eig", "k","p"))
res <- snow::parLapply(clus, 1:k, function(x) eig(n, p))
# 关闭多进程
snow::stopCluster(clus)
resDf <- do.call(rbind, res)
})
user system elapsed
0.01 0.02 2.40
head(resDf)
1.656100 | 1.640637 | 1.616798 | 1.586866 | 1.566930 | 1.538374 | 1.523015 | 1.495004 | 1.488897 | 1.468040 | ... | 0.5862909 | 0.5762028 | 0.5741423 | 0.5644911 | 0.5384094 | 0.5363696 | 0.5235116 | 0.5159777 | 0.4964601 | 0.4866267 |
1.691235 | 1.658395 | 1.609777 | 1.575012 | 1.558377 | 1.539480 | 1.522883 | 1.496592 | 1.489386 | 1.458325 | ... | 0.5896276 | 0.5873841 | 0.5737916 | 0.5722187 | 0.5611733 | 0.5579417 | 0.5285259 | 0.5217362 | 0.5153633 | 0.4782889 |
1.686425 | 1.653121 | 1.628179 | 1.598551 | 1.579656 | 1.557433 | 1.537228 | 1.498753 | 1.489975 | 1.460616 | ... | 0.5952069 | 0.5823180 | 0.5720212 | 0.5681074 | 0.5551917 | 0.5450221 | 0.5319406 | 0.5268110 | 0.5173417 | 0.5001969 |
1.713829 | 1.676294 | 1.613605 | 1.579971 | 1.567225 | 1.545787 | 1.519495 | 1.494373 | 1.471482 | 1.467215 | ... | 0.5946169 | 0.5853068 | 0.5719170 | 0.5502586 | 0.5498509 | 0.5435140 | 0.5358413 | 0.5112063 | 0.4937276 | 0.4767415 |
1.677933 | 1.615744 | 1.594095 | 1.577702 | 1.561510 | 1.544389 | 1.499172 | 1.491493 | 1.471160 | 1.449174 | ... | 0.6070764 | 0.6024897 | 0.5899197 | 0.5707263 | 0.5591482 | 0.5421557 | 0.5243488 | 0.5172421 | 0.5034002 | 0.4930905 |
1.659066 | 1.629982 | 1.612570 | 1.582731 | 1.570251 | 1.535848 | 1.517782 | 1.507463 | 1.494994 | 1.487952 | ... | 0.5987651 | 0.5832903 | 0.5745790 | 0.5577187 | 0.5494924 | 0.5432500 | 0.5378630 | 0.5128477 | 0.5062455 | 0.4900256 |
# library(doParallel)
cl<- snow::makeCluster(5) # 不过R中能运行满线程 ,即detectCores()返回的核数。
## 登记内核数量
doParallel::registerDoParallel(cl)
system.time(expr={x <- foreach(i=1:1000)%dopar%function(x){x+2}})
doParallel::stopImplicitCluster()
snow::stopCluster(cl)
user system elapsed
0.87 0.31 1.19
上面简单的测试,发现向量化的简单操作snow似乎优势更大,foreach循环次数到10000000电脑基本卡住了。如果是做复杂计算,似乎foreach更合适些。实际应用可以做个测试看看~
但是snow有个挺麻烦的事儿,需要把用到的变量和函数clusterExport,这个操作挺不人性化的。实际中用过来,感觉还是foreach的并行化处理更方便些。
Ref
[1] R语言实战第二版P444
[2] https://zhuanlan.zhihu.com/p/31998150
[3] https://blog.csdn.net/wa2003/article/details/48145147
[4] https://zsccy.xyz/md/2018-04-08-r%E8%AF%AD%E8%A8%80%E5%B9%B6%E8%A1%8C%E5%8C%96%E8%AE%A1%E7%AE%97%E4%B9%8Bforeach%E5%8C%85/
2020-04-22 于南京市江宁区九龙湖