Подтвердить что ты не робот

Почему параллельный пакет работает медленнее, чем просто использовать?

Я пытаюсь определить, когда использовать пакет parallel, чтобы ускорить время, необходимое для выполнения некоторого анализа. Одна из вещей, которые мне нужно сделать, - это создать матрицы, сравнивающие переменные в двух фреймах данных с различным количеством строк. Я задал вопрос об эффективном способе работы в Qaru и написал о тестах в своем блоге. Поскольку я доволен лучшим подходом, я хотел ускорить процесс, запустив его параллельно. Приведенные ниже результаты основаны на 2 ГГц i7 Mac с 8 ГБ оперативной памяти. Я удивлен, что пакет parallel, в частности, функция parSapply, хуже, чем просто использование функции apply. Код для воспроизведения этого ниже. Обратите внимание, что в настоящее время я использую только один из двух создаваемых мной столбцов, но в итоге хочу использовать оба.

Execution Time
(источник: bryer.org)

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)')
4b9b3361

Ответ 1

Выполнение заданий параллельно приводит к накладным расходам. Только если рабочие места, которые вы запускаете на рабочих узлах, занимаете значительное количество времени, распараллеливание повышает общую производительность. Когда отдельные задания занимают всего миллисекунды, накладные расходы на постоянное отключение рабочих мест ухудшат общую производительность. Трюк состоит в том, чтобы разделить работу над узлами таким образом, чтобы задания были достаточно длинными, скажем, по крайней мере несколько секунд. Я использовал это с большим успехом, работая одновременно с шестью моделями Fortran, но эти отдельные модели выполняли часы, почти отрицая эффект накладных расходов.

Обратите внимание, что я не запускал ваш пример, но ситуация, о которой я рассказываю выше, часто является проблемой, когда парализация занимает больше времени, чем выполнение последовательно.

Ответ 2

Эти различия могут быть связаны с 1) коммуникационными издержками (особенно, если вы используете узлы) и 2) служебные данные по производительности (если ваша работа не такая интенсивная по сравнению с инициированием параллелизации, например). Обычно, если задача, которую вы распараллеливаете, не требует много времени, тогда вы в основном обнаружите, что параллелизация не имеет большого эффекта (что очень заметно на огромных наборах данных.

Несмотря на то, что это не может напрямую ответить на ваш бенчмаркинг, я надеюсь, что это должно быть довольно простым и связанным с ним. В качестве примера здесь я построю строки data.frame с 1e6 с 1e4 уникальными столбцами group и некоторыми значениями в столбце val. И затем я запускаю с помощью plyr в parallel с помощью doMC и без распараллеливания.

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

Как вы можете видеть, параллельная версия plyr работает в 3,5 раза медленнее

Теперь позвольте мне использовать тот же data.frame, но вместо вычисления sum позвольте мне построить более сложную функцию, скажем, median(.) * median(rnorm(1e4) ((бессмысленно, да):

Вы увидите, что приливы начинают смещаться:

# 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

Здесь версия parallel имеет 1.752 times быстрее, чем непараллельная версия.

Изменить: Следуя комментарию @Paul, я просто выполнил небольшую задержку с помощью Sys.sleep(). Конечно, результаты очевидны. Но только ради полноты, вот результат на 20 * 2. data.frame:

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

Разница здесь не удивительна.

Ответ 3

Полностью согласен с аргументами @Arun и @PaulHiemestra о том, почему...? часть вашего вопроса.

Однако, похоже, вы можете воспользоваться некоторыми преимуществами пакета parallel в своей ситуации (по крайней мере, если вы не застряли в Windows). Возможное решение использует mclapply вместо parSapply, который опирается на быструю динамическую и общую память.

  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))
    })
  })

Конечно, вложенный system.time здесь не нужен. С моими 2 ядрами я получил:

enter image description here