Usado como soporte para computación paralela, esto puede ser implementado mediante: forking (tomado del paquete multicore), sockets (tomado del paquete snow) y generación de números aleatorios.
Además cada uno de ellos tiene sus ventajas y desventajas: 1. El primero funciona en cualquier sistema (incluido Windows), cosa que el segúndo solo en sistemas de tipo POSIX. 2. El segundo es más fácil de implentar, que el socket.
library(parallel)
La función para iniciar un cluster es makeCluster, que toma como argumento el número de núcleos:
numCores <- detectCores()
numCores
## [1] 8
La función toma como argumento un tipo que puede ser PSOCK (la versión socket) o FORK (la versión fork). Generalmente, mclapply debería usarse para el enfoque de bifurcación, así que no hay necesidad de cambiar esto.
Cuando se utiliza el enfoque de socket para el procesamiento paralelo, cada proceso se inicia fresco, por lo que cosas como paquetes cargados y cualquier variable existente en su sesión actual no existen. En su lugar, debemos moverlas a cada proceso.
La forma más genérica de hacer esto es la función clusterEvalQ, que toma un cluster y cualquier expresión, y ejecuta la expresión en cada proceso.
cl <- makeCluster(numCores)
clusterEvalQ(cl, library(lme4))
## [[1]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[2]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[3]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[4]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[5]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[6]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[7]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[8]]
## [1] "lme4" "Matrix" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
Existen versiones paralelas de las tres sentencias apply principales: parApply, parLapply y parSapply para apply, lapply y sapply respectivamente. Toman un argumento adicional para el cluster sobre el que operar.
Cronometrar esto es complicado, si sólo cronometramos la llamada a parLapply no estamos capturando el tiempo para abrir y cerrar el cluster, y si cronometramos todo, estamos incluyendo la llamada a lme4. Para ser completamente justos, necesitamos incluir la carga de lme4 en los tres casos.
library(parallel)
f <- function(i) {
lmer(Petal.Width ~ . - Species + (1 | Species), data = iris)
}
system.time({
library(lme4)
save1 <- lapply(1:100, f)
})
## user system elapsed
## 7.81 0.94 14.99
library(parallel)
f <- function(i) {
lmer(Petal.Width ~ . - Species + (1 | Species), data = iris)
}
system.time({
library(lme4)
save2 <- mclapply(1:100, f)
})
## user system elapsed
## 4.36 0.17 5.53
library(parallel)
f <- function(i) {
lmer(Petal.Width ~ . - Species + (1 | Species), data = iris)
}
system.time({
cl <- makeCluster(detectCores())
clusterEvalQ(cl, library(lme4))
save3 <- parLapply(cl, 1:100, f)
stopCluster(cl)
})
## user system elapsed
## 0.17 0.37 22.62
Esto no es totalmente necesario, pero es una buena práctica. Si no se detiene, los procesos continúan ejecutándose en segundo plano, consumiendo recursos, y cualquier nuevo proceso puede ser ralentizado o retrasado. Si sale de R, también debería cerrar automáticamente todos los procesos. Esto no elimina el objeto, que en este caso sería el cl, sólo el cluster al que se refiere en segundo plano.
# stopCluster(cl)