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次任务都白跑了,如果已经跑了几小时的任务毁在了一个循环的报错,砸键盘的心情都有了。此时可以修改.errorhandling
为remove
,跳过报错的循环,例如
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
可以看到,第五次报错的循环被跳过了。当然,我们还是希望找出报错问题所在并尝试解决,此时可以修改.errorhandling
为pass
:
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_GlobalEnv
,f()
在其自身环境内创建的局部变量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