Как оптимизировать рекурсивную функцию для поиска всех перестановок? - PullRequest
2 голосов
/ 27 апреля 2019

Я написал следующий фрагмент кода, чтобы найти все перестановки данного вектора:

perm <- function(v, r = NULL, P = NULL) {
  l <- length(v)
  if (l == 0) {
    P <- rbind(P, r)
    rownames(P) <- NULL
    P
  } else {
    for (i in 1:l) {
      new_r <- c(r, v[i])
      new_v <- v[-i]
      P <- perm(new_v, new_r, P)
    }
    P
  }
}

P <- perm(1:9) # takes "forever" yet e.g. perm(1:7) is quite fast!?!
P

Он делает то, что должен, но проблема в том, что он работает вечно, если использовать векторы длиной> 8 (как указано выше).

Мой вопрос
Я не вижу проблемы, я обнаружил некоторые рекурсивные реализации, которые не выглядят так по-разному, но гораздо более эффективны ... Так есть ли простой способ оптимизировать код, чтобы он работал быстрее?

Ответы [ 2 ]

2 голосов
/ 27 апреля 2019

Как утверждает @akrun, рекурсия в R обычно не так эффективна.Однако, если вам нужно рекурсивное решение, смотрите не дальше gtools::permutations.Вот реализация:

permGtools <- function(n, r, v) {
    if (r == 1) 
        matrix(v, n, 1)
    else if (n == 1) 
        matrix(v, 1, r)
    else {
        X <- NULL
        for (i in 1:n) X <- rbind(X, cbind(v[i], permGtools(n - 1, r - 1, v[-i])))
        X
    }
}

Кстати, чтобы получить полный исходный код, просто наберите gtools::permutations в консоли и нажмите Enter.Для получения дополнительной информации см. Как просмотреть исходный код функции?

А вот некоторые временные параметры:

system.time(perm(1:8))
  user  system elapsed 
34.074  10.641  44.815

system.time(permGtools(8,8,1:8))
 user  system elapsed 
0.253   0.001   0.255

И просто для примера:

system.time(permGtools(9, 9, 1:9))
 user  system elapsed 
2.512   0.046   2.567


Почему реализация OP медленнее?

Если вы не читаете подробности, перейдите к сводке.

ДляДля начала мы можем просто увидеть, что реализация OP делает более рекурсивные вызовы, чем реализация в gtools.Чтобы показать это, мы добавляем count <<- count + 1L в начало каждой функции (примечание. Мы используем оператор присваивания <<-, который сначала ищет в родительской среде).Например:

permGtoolsCount <- function(n, r, v) {
    count <<- count + 1L
    if (r == 1)
    .
    .

А теперь мы протестируем несколько длин:

iterationsOP <- sapply(4:7, function(x) {
    count <<- 0L
    temp <- permCount(1:x)
    count
})

iterationsOP
[1]    65   326  1957 13700

iterationsGtools <- sapply(4:7, function(x) {
    count <<- 0L
    temp <- permGtoolsCount(x, x, 1:x)
    count
})

iterationsGtools
[1]   41  206 1237 8660

Как видите, реализация OP делает больше вызовов в каждом случае.На самом деле, он в 1033 * раз превышает количество рекурсивных вызовов.

iterationsOP / iterationsGtools
[1] 1.585366 1.582524 1.582053 1.581986

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

На данный момент, трудно поверить, что создание примерно в 1,58 раза большеРекурсивные вызовы объясняют ускорение в 175 раз, которое мы видели выше (т. е. 44.815 / 0.255 ~= 175).

Мы можем профилировать код с помощью Rprof, чтобы получить дополнительную информацию:

Rprof("perm.out", memory.profiling = TRUE)
a1 <- perm(1:8)
Rprof(NULL)
summaryRprof("perm.out", memory = "both")$by.total
             total.time total.pct mem.total self.time self.pct
"perm"            43.42    100.00   15172.1      0.58     1.34
"rbind"           22.50     51.82    7513.7     22.50    51.82
"rownames<-"      20.32     46.80    7388.7     20.30    46.75
"c"                0.02      0.05      23.7      0.02     0.05
"length"           0.02      0.05       0.0      0.02     0.05



Rprof("permGtools.out", memory.profiling = TRUE)
a2 <- permGtools(8, 8, 1:8)
Rprof(NULL)
summaryRprof("permGtools.out", memory = "tseries")$by.total
             total.time total.pct mem.total self.time self.pct
"rbind"            0.34    100.00     134.8      0.18    52.94
"cbind"            0.34    100.00     134.8      0.08    23.53
"permGtools"       0.34    100.00     134.8      0.06    17.65
"matrix"           0.02      5.88       0.0      0.02     5.88

Одна вещь, которая выскакивает сразу (кроме времени), это огромное использование памяти реализацией OP.Реализация OP использует примерно 15 Гбайт памяти, тогда как реализация gtools использует только 134 Мб .

Копать глубже

В приведенном вышемы просто смотрим на использование памяти в общем виде, установив для параметра памяти значение both.Есть еще одна настройка, которая называется tseries, которая позволяет вам отслеживать использование памяти с течением времени.

head(summaryRprof("perm.out", memory = "tseries"))
     vsize.small vsize.large    nodes duplications       stack:2
0.02     4050448    25558992 49908432         2048 "perm":"perm"
0.04       98808    15220400  1873760          780 "perm":"perm"
0.06       61832    12024184  1173256          489 "perm":"perm"
0.08       45400           0   861728          358 "perm":"perm"
0.1            0    14253568        0          495 "perm":"perm"
0.12       75752    21412320  1436120          599 "perm":"perm"

head(summaryRprof("permGtools.out", memory = "tseries"))
     vsize.small vsize.large    nodes duplications              stack:2
0.02     4685464    39860824 43891512            0 "permGtools":"rbind"
0.04      542080      552384 12520256            0 "permGtools":"rbind"
0.06           0           0        0            0 "permGtools":"rbind"
0.08      767992     1200864 17740912            0 "permGtools":"rbind"
0.1       500208      566592 11561312            0 "permGtools":"rbind"
0.12           0      151488        0            0 "permGtools":"rbind"

Здесь много чего происходит, но на чем стоит остановиться, это поле duplications.Из документации на summaryRprof мы имеем:

Также записывается количество повторных вызовов внутренней функции во временном интервале.duplicate вызывается кодом C, когда необходимо скопировать аргументы.

Сравнение количества копий в каждой реализации:

sum(summaryRprof("perm.out", memory = "tseries")$duplications)
[1] 121006

sum(summaryRprof("permGtools.out", memory = "tseries")$duplications)
[1] 0

Итак, мы видим, что реализация OP требует много копийбыть произведенным.Я предполагаю, что это не удивительно, учитывая, что нужный объект является параметром в прототипе функции.Таким образом, P - это матрица перестановок, которая должна быть возвращена, и она постоянно увеличивается и увеличивается с каждой итерацией.И с каждой итерацией мы передаем ее perm.В реализации gtools вы заметите, что это не так, поскольку это просто два числовых значения и вектор для его параметров.

Сводка

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

2 голосов
/ 27 апреля 2019

Может быть лучше использовать permGeneral из RcppAlgos

P <- perm(1:5) # OP's function

library(RcppAlgos)
P1 <- permuteGeneral(5, 5)
all.equal(P, P1, check.attributes = FALSE)
#[1] TRUE  

Контрольные отметки

Для чуть более длинной последовательности

system.time({

   P2 <- permuteGeneral(8, 8)
  })
#user  system elapsed 
#  0.001   0.000   0.001 

system.time({

   P20 <- perm(1:8) #OP's function
})
# user  system elapsed 
# 31.254  11.045  42.226 

all.equal(P2, P20, check.attributes = FALSE)
#[1] TRUE

Обычно, рекурсивная функция может занять больше времени, так как рекурсивные вызовы этой функции занимают больше времени

...