Perché il pacchetto parallelo è più lento del semplice utilizzo di apply?

Sto cercando di determinare quando utilizzare il pacchetto parallel per accelerare il tempo necessario per eseguire alcune analisi. Una delle cose che devo fare è creare matrici che confrontino le variabili in due frame di dati con un numero di righe diverso. Ho fatto una domanda su un modo efficiente di fare su StackOverflow e ho scritto sui test sul mio blog . Dal momento che mi sento a mio agio con l’approccio migliore, volevo accelerare il processo eseguendolo in parallelo. I risultati di seguito sono basati su un Mac i7 con 2 GB e 8 GB di RAM. Sono sorpreso che il pacchetto parallel , in particolare la funzione parSapply , sia peggio dell’utilizzo della funzione apply . Il codice per replicare questo è sotto. Nota che attualmente sto usando solo una delle due colonne che creo ma alla fine voglio usarle entrambe.

Tempo di esecuzione http://sofit.miximages.com/r/ParalleVsApplyTiming.png

 require(parallel) require(ggplot2) require(reshape2) set.seed(2112) results <- list() sizes <- seq(1000, 30000, by=5000) pb <- txtProgressBar(min=0, max=length(sizes), style=3) for(cnt in 1:length(sizes)) { i <- sizes[cnt] df1 <- data.frame(row.names=1:i, var1=sample(c(TRUE,FALSE), i, replace=TRUE), var2=sample(1:10, i, replace=TRUE) ) df2 <- data.frame(row.names=(i + 1):(i + i), var1=sample(c(TRUE,FALSE), i, replace=TRUE), var2=sample(1:10, i, replace=TRUE)) tm1 <- system.time({ df6 <- sapply(df2$var1, FUN=function(x) { x == df1$var1 }) dimnames(df6) <- list(row.names(df1), row.names(df2)) }) rm(df6) tm2 <- system.time({ cl <- makeCluster(getOption('cl.cores', detectCores())) tm3 <- system.time({ df7 <- parSapply(cl, df1$var1, FUN=function(x, df2) { x == df2$var1 }, df2=df2) dimnames(df7) <- list(row.names(df1), row.names(df2)) }) stopCluster(cl) }) rm(df7) results[[cnt]] <- c(apply=tm1, parallel.total=tm2, parallel.exec=tm3) setTxtProgressBar(pb, cnt) } toplot <- as.data.frame(results)[,c('apply.user.self','parallel.total.user.self', 'parallel.exec.user.self')] toplot$size <- sizes toplot <- melt(toplot, id='size') ggplot(toplot, aes(x=size, y=value, colour=variable)) + geom_line() + xlab('Vector Size') + ylab('Time (seconds)') 

L’esecuzione di lavori in parallelo comporta un sovraccarico. Solo se i lavori che si svolgono nei nodes di lavoro richiedono una quantità significativa di tempo, la parallelizzazione migliora le prestazioni generali. Quando i singoli lavori impiegano solo millisecondi, il sovraccarico di un licenziamento costante dei lavori peggiorerà le prestazioni complessive. Il trucco è dividere il lavoro sui nodes in modo tale che i lavori siano sufficientemente lunghi, diciamo almeno qualche secondo. L’ho usato con grande efficacia eseguendo sei modelli Fortran contemporaneamente, ma questi modelli individuali hanno impiegato ore, quasi a negare l’effetto del sovraccarico.

Si noti che non ho eseguito il tuo esempio, ma la situazione che descrivo sopra è spesso il problema quando la parallizzazione richiede più tempo rispetto all’esecuzione sequenziale.

Queste differenze possono essere attribuite a 1) overhead di comunicazione (soprattutto se si attraversano i nodes) e 2) overhead delle prestazioni (se il lavoro non è così intenso rispetto all’avvio di una parallelizzazione, ad esempio). Di solito, se l’attività che si sta parallelizzando non richiede molto tempo, allora si scoprirà soprattutto che la parallelizzazione NON ha molto effetto (che è molto visibile su enormi set di dati.

Anche se questo potrebbe non rispondere direttamente al tuo benchmark, spero che questo dovrebbe essere piuttosto semplice e può essere collegato a. Ad esempio, qui costruisco un data.frame con 1e6 righe con 1e4 voci di un group colonne univoche e alcuni valori nella colonna val . E poi corro usando plyr in parallel usando doMC e senza parallelizzazione.

 df <- data.frame(group = as.factor(sample(1:1e4, 1e6, replace = T)), val = sample(1:10, 1e6, replace = T)) > head(df) group val # 1 8498 8 # 2 5253 6 # 3 1495 1 # 4 7362 9 # 5 2344 6 # 6 5602 9 > dim(df) # [1] 1000000 2 require(plyr) require(doMC) registerDoMC(20) # 20 processors # parallelisation using doMC + plyr P.PLYR <- function() { o1 <- ddply(df, .(group), function(x) sum(x$val), .parallel = TRUE) } # no parallelisation PLYR <- function() { o2 <- ddply(df, .(group), function(x) sum(x$val), .parallel = FALSE) } require(rbenchmark) benchmark(P.PLYR(), PLYR(), replications = 2, order = "elapsed") test replications elapsed relative user.self sys.self user.child sys.child 2 PLYR() 2 8.925 1.000 8.865 0.068 0.000 0.000 1 P.PLYR() 2 30.637 3.433 15.841 13.945 8.944 38.858 

Come puoi vedere, la versione parallela di plyr funziona 3,5 volte più lentamente

Ora, permettimi di usare lo stesso data.frame , ma invece di calcolare la sum , permettimi di build una funzione un po 'più impegnativa, ad esempio, median(.) * median(rnorm(1e4) ((senza significato, sì):

Vedrai che le maree cominciano a cambiare:

 # parallelisation using doMC + plyr P.PLYR <- function() { o1 <- ddply(df, .(group), function(x) median(x$val) * median(rnorm(1e4)), .parallel = TRUE) } # no parallelisation PLYR <- function() { o2 <- ddply(df, .(group), function(x) median(x$val) * median(rnorm(1e4)), .parallel = FALSE) } > benchmark(P.PLYR(), PLYR(), replications = 2, order = "elapsed") test replications elapsed relative user.self sys.self user.child sys.child 1 P.PLYR() 2 41.911 1.000 15.265 15.369 141.585 34.254 2 PLYR() 2 73.417 1.752 73.372 0.052 0.000 0.000 

Qui, la versione parallela è 1.752 times più veloce rispetto alla versione non parallela.

Modifica: seguendo il commento di @ Paul, ho implementato un piccolo ritardo usando Sys.sleep() . Ovviamente i risultati sono ovvi. Ma solo per completezza, ecco il risultato su un data.frame 20 * 2:

 df <- data.frame(group=sample(letters[1:5], 20, replace=T), val=sample(20)) # parallelisation using doMC + plyr P.PLYR <- function() { o1 <- ddply(df, .(group), function(x) { Sys.sleep(2) median(x$val) }, .parallel = TRUE) } # no parallelisation PLYR <- function() { o2 <- ddply(df, .(group), function(x) { Sys.sleep(2) median(x$val) }, .parallel = FALSE) } > benchmark(P.PLYR(), PLYR(), replications = 2, order = "elapsed") # test replications elapsed relative user.self sys.self user.child sys.child # 1 P.PLYR() 2 4.116 1.000 0.056 0.056 0.024 0.04 # 2 PLYR() 2 20.050 4.871 0.028 0.000 0.000 0.00 

La differenza qui non è sorprendente.

Completamente d’accordo con gli argomenti @Arun e @PaulHiemestra riguardanti Perché …? parte della tua domanda.

Tuttavia, sembra che tu possa trarre alcuni vantaggi dal pacchetto parallel nella tua situazione (almeno se non sei bloccato con Windows). La soluzione ansible è l’utilizzo di mclapply invece di parSapply , che si basa su forking rapido e memoria condivisa.

  tm2 <- system.time({ tm3 <- system.time({ df7 <- matrix(unlist(mclapply(df2$var1, FUN=function(x) {x==df1$var1}, mc.cores=8)), nrow=i) dimnames(df7) <- list(row.names(df1), row.names(df2)) }) }) 

Ovviamente, nested system.time non è necessario qui. Con i miei 2 core ho ottenuto:

inserisci la descrizione dell'immagine qui