... относительно времени выполнения и/или памяти.
Если это неверно, докажите это с помощью фрагмента кода. Обратите внимание, что ускорение путем векторизации не учитывается. Ускорение должно происходить от apply
(tapply
, sapply
,...).
... относительно времени выполнения и/или памяти.
Если это неверно, докажите это с помощью фрагмента кода. Обратите внимание, что ускорение путем векторизации не учитывается. Ускорение должно происходить от apply
(tapply
, sapply
,...).
Функции apply
в R не обеспечивают улучшенную производительность по сравнению с другими функциями циклирования (например, for
). Единственное исключение - lapply
, которое может быть немного быстрее, потому что оно больше работает в коде C, чем в R (см. этот вопрос для примера этого).
Но в целом правило состоит в том, что вы должны использовать функцию apply для ясности, а не для производительности.
Я бы добавил к этому, что применять функции никаких побочных эффектов, что является важным отличием, когда оно приходит к функциональному программированию с R. Это можно переопределить с помощью assign
или <<-
, но это может быть очень опасно. Побочные эффекты также затрудняют понимание программы, поскольку переменное состояние зависит от истории.
Edit:
Просто подчеркнем это с помощью тривиального примера, который рекурсивно вычисляет последовательность Фибоначчи; это можно запустить несколько раз, чтобы получить точную меру, но дело в том, что ни один из методов не имеет значительно различной производительности:
> fibo <- function(n) {
+ if ( n < 2 ) n
+ else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
user system elapsed
7.48 0.00 7.52
> system.time(sapply(0:26, fibo))
user system elapsed
7.50 0.00 7.54
> system.time(lapply(0:26, fibo))
user system elapsed
7.48 0.04 7.54
> library(plyr)
> system.time(ldply(0:26, fibo))
user system elapsed
7.52 0.00 7.58
Изменить 2:
Что касается использования параллельных пакетов для R (например, rpvm, rmpi, snow), они обычно предоставляют семейные функции apply
(даже пакет foreach
по существу эквивалентен, несмотря на имя). Здесь простой пример функции sapply
в snow
:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
В этом примере используется кластер сокетов, для которого не требуется устанавливать дополнительное программное обеспечение; в противном случае вам понадобится что-то вроде PVM или MPI (см. страница кластеризации Tierney). snow
имеет следующие применимые функции:
parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)
Имеет смысл, что функции apply
должны использоваться для параллельного выполнения, поскольку у них нет побочных эффектов. Когда вы изменяете значение переменной в цикле for
, оно устанавливается глобально. С другой стороны, все функции apply
можно безопасно использовать параллельно, потому что изменения являются локальными для вызова функции (если вы не пытаетесь использовать assign
или <<-
, и в этом случае вы можете ввести побочные эффекты). Излишне говорить, что важно быть осторожным в отношении локальных и глобальных переменных, особенно при параллельном выполнении.
Edit:
Вот тривиальный пример, демонстрирующий разницу между for
и *apply
в отношении побочных эффектов:
> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
[1] 1 2 3 4 5 6 7 8 9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
[1] 6 12 18 24 30 36 42 48 54 60
Обратите внимание, что df
в родительской среде изменяется на for
, но не *apply
.
Иногда ускорение может быть существенным, например, когда вам нужно встраивать for-loops, чтобы получить среднее значение, основанное на группировке более чем одного фактора. Здесь у вас есть два подхода, которые дают вам тот же результат:
set.seed(1) #for reproducability of the results
# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions
#levels() and length() don't have to be called more than once.
ylev <- levels(y)
zlev <- levels(z)
n <- length(ylev)
p <- length(zlev)
out <- matrix(NA,ncol=p,nrow=n)
for(i in 1:n){
for(j in 1:p){
out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
}
}
rownames(out) <- ylev
colnames(out) <- zlev
return(out)
}
# Used on the generated data
forloop(X,Y,Z)
# The same using tapply
tapply(X,list(Y,Z),mean)
Оба дают точно такой же результат, будучи матрицей 5 × 10 со средними значениями и именованными строками и столбцами. Но:
> system.time(forloop(X,Y,Z))
user system elapsed
0.94 0.02 0.95
> system.time(tapply(X,list(Y,Z),mean))
user system elapsed
0.06 0.00 0.06
Там вы идете. Что я выиграл?; -)
... и, как я только что написал в другом месте, vapply - ваш друг! ... это похоже на sapply, но вы также указываете тип возвращаемого значения, который делает его намного быстрее.
> system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
user system elapsed
3.54 0.00 3.53
> system.time(z <- lapply(y, foo))
user system elapsed
2.89 0.00 2.91
> system.time(z <- vapply(y, foo, numeric(1)))
user system elapsed
1.35 0.00 1.36
Я писал в другом месте, что пример, подобный Шейну, на самом деле не подчеркивает разницу в производительности среди различных типов синтаксиса цикла, потому что время все расходуется внутри функции, а не на самом деле подчеркивает цикл. Кроме того, код несправедливо сравнивает цикл for без памяти с применением семейных функций, которые возвращают значение. Вот несколько другой пример, который подчеркивает точку.
foo <- function(x) {
x <- x+1
}
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
# user system elapsed
# 4.967 0.049 7.293
system.time(z <- sapply(y, foo))
# user system elapsed
# 5.256 0.134 7.965
system.time(z <- lapply(y, foo))
# user system elapsed
# 2.179 0.126 3.301
Если вы планируете сохранить результат, то применять семейные функции может быть намного больше, чем синтаксический сахар.
(простой список из z составляет всего 0,2 с, поэтому сложение происходит намного быстрее. Инициализация z в цикле for выполняется довольно быстро, потому что я даю среднее значение последних 5 из 6 прогонов, так что перемещение за пределами системы .time вряд ли повлияет на вещи)
Еще одна вещь, которую следует отметить, заключается в том, что есть еще одна причина использовать функции семейства, независимо от их производительности, ясности или отсутствия побочных эффектов. A for
цикл обычно способствует помещению как можно большего числа в цикл. Это связано с тем, что каждый цикл требует установки переменных для хранения информации (среди других возможных операций). Применить заявления имеют тенденцию быть предвзятыми по-другому. Часто вы хотите выполнять несколько операций над вашими данными, некоторые из которых могут быть векторизованы, но некоторые из них могут быть недоступны. В R, в отличие от других языков, лучше всего отделить эти операции и запустить те, которые не векторизованы в заявлении приложения (или векторизованной версии функции), и те, которые векторизованы как действительные векторные операции. Это часто ускоряет производительность.
Взяв пример Joris Meys, где он заменяет традиционную для цикла удобную функцию R, мы можем использовать ее, чтобы показать эффективность написания кода более дружественным образом R для подобного ускорения без специализированной функции.
set.seed(1) #for reproducability of the results
# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))
# an R way to generate tapply functionality that is fast and
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m
Это происходит быстрее, чем цикл for
и немного медленнее, чем встроенная оптимизированная функция tapply
. Это не потому, что vapply
намного быстрее, чем for
, а потому, что он выполняет только одну операцию на каждой итерации цикла. В этом коде все остальное векторизовано. В традиционном цикле for
Joris Meys на каждой итерации происходит множество (7?) Операций, и для ее выполнения достаточно немного настроек. Заметьте также, насколько это компактнее, чем версия for
.
При применении функций над подмножествами вектора tapply
может быть довольно быстрым, чем цикл for. Пример:
df <- data.frame(id = rep(letters[1:10], 100000),
value = rnorm(1000000))
f1 <- function(x)
tapply(x$value, x$id, sum)
f2 <- function(x){
res <- 0
for(i in seq_along(l <- unique(x$id)))
res[i] <- sum(x$value[x$id == l[i]])
names(res) <- l
res
}
library(microbenchmark)
> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
expr min lq median uq max neval
f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656 100
f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273 100
apply
, однако в большинстве случаев не обеспечивается увеличение скорости, а в некоторых случаях может быть еще медленнее:
mat <- matrix(rnorm(1000000), nrow=1000)
f3 <- function(x)
apply(x, 2, sum)
f4 <- function(x){
res <- 0
for(i in 1:ncol(x))
res[i] <- sum(x[,i])
res
}
> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975 100
f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100 100
Но для этих ситуаций у нас есть colSums
и rowSums
:
f5 <- function(x)
colSums(x)
> microbenchmark(f5(mat), times=100)
Unit: milliseconds
expr min lq median uq max neval
f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909 100