Действительно ли семейство "* apply " не векторизовано?
поэтому мы привыкли говорить каждому новому пользователю R, что"apply
не векторизован, проверьте Патрик Бернс R Инферно круг 4 " который говорит (цитирую):
общим рефлексом является использование функции в семействе apply. это не векторизация, это скрытие петли. Применить функцию для цикла в его определение. Функция lapply хоронит цикл, но выполнение раз, как правило примерно равно явному циклу for.
действительно, быстрый взгляд на apply
исходный код показывает цикл:
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
ок до сих пор, но посмотрите на lapply
или vapply
на самом деле показывает совершенно иную картину:
lapply
## function (X, FUN, ...)
## {
## FUN <- match.fun(FUN)
## if (!is.vector(X) || is.object(X))
## X <- as.list(X)
## .Internal(lapply(X, FUN))
## }
## <bytecode: 0x000000000284b618>
## <environment: namespace:base>
так что, по-видимому, нет R for
цикл скрывается там, а они вызывают внутреннюю функцию C written.
быстрый взгляд в кроликдыре показывает в значительной степени та же картина
более того, давайте возьмем colMeans
функция, например, которая никогда не обвинялась в том, что не векторизована
colMeans
# function (x, na.rm = FALSE, dims = 1L)
# {
# if (is.data.frame(x))
# x <- as.matrix(x)
# if (!is.array(x) || length(dn <- dim(x)) < 2L)
# stop("'x' must be an array of at least two dimensions")
# if (dims < 1L || dims > length(dn) - 1L)
# stop("invalid 'dims'")
# n <- prod(dn[1L:dims])
# dn <- dn[-(1L:dims)]
# z <- if (is.complex(x))
# .Internal(colMeans(Re(x), n, prod(dn), na.rm)) + (0+1i) *
# .Internal(colMeans(Im(x), n, prod(dn), na.rm))
# else .Internal(colMeans(x, n, prod(dn), na.rm))
# if (length(dn) > 1L) {
# dim(z) <- dn
# dimnames(z) <- dimnames(x)[-(1L:dims)]
# }
# else names(z) <- dimnames(x)[[dims + 1]]
# z
# }
# <bytecode: 0x0000000008f89d20>
# <environment: namespace:base>
да? Он также просто называет .Internal(colMeans(...
которых мы также можем найти в кроличья нора. Так чем же это отличается от .Internal(lapply(..
?
на самом деле быстрый тест показывает, что sapply
выполняет не хуже, чем colMeans
и гораздо лучше, чем for
петли для больших данных набор
m <- as.data.frame(matrix(1:1e7, ncol = 1e5))
system.time(colMeans(m))
# user system elapsed
# 1.69 0.03 1.73
system.time(sapply(m, mean))
# user system elapsed
# 1.50 0.03 1.60
system.time(apply(m, 2, mean))
# user system elapsed
# 3.84 0.03 3.90
system.time(for(i in 1:ncol(m)) mean(m[, i]))
# user system elapsed
# 13.78 0.01 13.93
другими словами, Правильно ли говорить, что lapply
и vapply
на самом деле векторизированный (по сравнению с apply
что это for
цикл, который также называет lapply
) и что на самом деле хотел сказать Патрик Бернс?
4 ответа:
во-первых, в вашем примере Вы делаете тесты на "данные.каркас", который не является справедливым для
colMeans
,apply
и"[.data.frame"
так как у них есть накладные расходы:system.time(as.matrix(m)) #called by `colMeans` and `apply` # user system elapsed # 1.03 0.00 1.05 system.time(for(i in 1:ncol(m)) m[, i]) #in the `for` loop # user system elapsed # 12.93 0.01 13.07
на матрице картинка немного другая:
mm = as.matrix(m) system.time(colMeans(mm)) # user system elapsed # 0.01 0.00 0.01 system.time(apply(mm, 2, mean)) # user system elapsed # 1.48 0.03 1.53 system.time(for(i in 1:ncol(mm)) mean(mm[, i])) # user system elapsed # 1.22 0.00 1.21
возвращаясь к основной части вопроса, основное различие между
lapply
/mapply
/ etc и простые R-петли-это то, где выполняется цикл. Как отмечает Роланд, оба цикла C и R должны оценивать функцию R в каждой итерации что является самым дорогостоящим. Действительно быстрые функции C-это те, которые делают все в C, поэтому, я думаю, это должно быть то, что "векторизовано"?пример, где мы находим среднее значение в каждом из элементов "списка" s:
(редактировать 11 мая ' 16: я считаю, что пример с поиском "среднего" не является хорошей настройкой для различий между итеративным вычислением функции R и скомпилированным кодом, (1) из-за особенности среднего значения R алгоритм на "числовом" s над простым
sum(x) / length(x)
и (2) это должно иметь больше смысла, чтобы проверить на "список"s сlength(x) >> lengths(x)
. Итак," средний " пример перемещается в конец и заменяется другим.)в качестве простого примера мы могли бы рассмотреть нахождение противоположности каждого
length == 1
элемент "список":на
tmp.c
file:#include <R.h> #define USE_RINTERNALS #include <Rinternals.h> #include <Rdefines.h> /* call a C function inside another */ double oppC(double x) { return(ISNAN(x) ? NA_REAL : -x); } SEXP sapply_oppC(SEXP x) { SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x))); for(int i = 0; i < LENGTH(x); i++) REAL(ans)[i] = oppC(REAL(VECTOR_ELT(x, i))[0]); UNPROTECT(1); return(ans); } /* call an R function inside a C function; * will be used with 'f' as a closure and as a builtin */ SEXP sapply_oppR(SEXP x, SEXP f) { SEXP call = PROTECT(allocVector(LANGSXP, 2)); SETCAR(call, install(CHAR(STRING_ELT(f, 0)))); SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x))); for(int i = 0; i < LENGTH(x); i++) { SETCADR(call, VECTOR_ELT(x, i)); REAL(ans)[i] = REAL(eval(call, R_GlobalEnv))[0]; } UNPROTECT(2); return(ans); }
и в сторону R:
system("R CMD SHLIB /home/~/tmp.c") dyn.load("/home/~/tmp.so")
С данные:
set.seed(007) myls = rep_len(as.list(c(NA, runif(3))), 1e7) #a closure wrapper of `-` oppR = function(x) -x for_oppR = compiler::cmpfun(function(x, f) { f = match.fun(f) ans = numeric(length(x)) for(i in seq_along(x)) ans[[i]] = f(x[[i]]) return(ans) })
бенчмаркинг:
#call a C function iteratively system.time({ sapplyC = .Call("sapply_oppC", myls) }) # user system elapsed # 0.048 0.000 0.047 #evaluate an R closure iteratively system.time({ sapplyRC = .Call("sapply_oppR", myls, "oppR") }) # user system elapsed # 3.348 0.000 3.358 #evaluate an R builtin iteratively system.time({ sapplyRCprim = .Call("sapply_oppR", myls, "-") }) # user system elapsed # 0.652 0.000 0.653 #loop with a R closure system.time({ forR = for_oppR(myls, "oppR") }) # user system elapsed # 4.396 0.000 4.409 #loop with an R builtin system.time({ forRprim = for_oppR(myls, "-") }) # user system elapsed # 1.908 0.000 1.913 #for reference and testing system.time({ sapplyR = unlist(lapply(myls, oppR)) }) # user system elapsed # 7.080 0.068 7.170 system.time({ sapplyRprim = unlist(lapply(myls, `-`)) }) # user system elapsed # 3.524 0.064 3.598 all.equal(sapplyR, sapplyRprim) #[1] TRUE all.equal(sapplyR, sapplyC) #[1] TRUE all.equal(sapplyR, sapplyRC) #[1] TRUE all.equal(sapplyR, sapplyRCprim) #[1] TRUE all.equal(sapplyR, forR) #[1] TRUE all.equal(sapplyR, forRprim) #[1] TRUE
(следует исходному примеру среднего нахождения):
#all computations in C all_C = inline::cfunction(sig = c(R_ls = "list"), body = ' SEXP tmp, ans; PROTECT(ans = allocVector(REALSXP, LENGTH(R_ls))); double *ptmp, *pans = REAL(ans); for(int i = 0; i < LENGTH(R_ls); i++) { pans[i] = 0.0; PROTECT(tmp = coerceVector(VECTOR_ELT(R_ls, i), REALSXP)); ptmp = REAL(tmp); for(int j = 0; j < LENGTH(tmp); j++) pans[i] += ptmp[j]; pans[i] /= LENGTH(tmp); UNPROTECT(1); } UNPROTECT(1); return(ans); ') #a very simple `lapply(x, mean)` C_and_R = inline::cfunction(sig = c(R_ls = "list"), body = ' SEXP call, ans, ret; PROTECT(call = allocList(2)); SET_TYPEOF(call, LANGSXP); SETCAR(call, install("mean")); PROTECT(ans = allocVector(VECSXP, LENGTH(R_ls))); PROTECT(ret = allocVector(REALSXP, LENGTH(ans))); for(int i = 0; i < LENGTH(R_ls); i++) { SETCADR(call, VECTOR_ELT(R_ls, i)); SET_VECTOR_ELT(ans, i, eval(call, R_GlobalEnv)); } double *pret = REAL(ret); for(int i = 0; i < LENGTH(ans); i++) pret[i] = REAL(VECTOR_ELT(ans, i))[0]; UNPROTECT(3); return(ret); ') R_lapply = function(x) unlist(lapply(x, mean)) R_loop = function(x) { ans = numeric(length(x)) for(i in seq_along(x)) ans[i] = mean(x[[i]]) return(ans) } R_loopcmp = compiler::cmpfun(R_loop) set.seed(007); myls = replicate(1e4, runif(1e3), simplify = FALSE) all.equal(all_C(myls), C_and_R(myls)) #[1] TRUE all.equal(all_C(myls), R_lapply(myls)) #[1] TRUE all.equal(all_C(myls), R_loop(myls)) #[1] TRUE all.equal(all_C(myls), R_loopcmp(myls)) #[1] TRUE microbenchmark::microbenchmark(all_C(myls), C_and_R(myls), R_lapply(myls), R_loop(myls), R_loopcmp(myls), times = 15) #Unit: milliseconds # expr min lq median uq max neval # all_C(myls) 37.29183 38.19107 38.69359 39.58083 41.3861 15 # C_and_R(myls) 117.21457 123.22044 124.58148 130.85513 169.6822 15 # R_lapply(myls) 98.48009 103.80717 106.55519 109.54890 116.3150 15 # R_loop(myls) 122.40367 130.85061 132.61378 138.53664 178.5128 15 # R_loopcmp(myls) 105.63228 111.38340 112.16781 115.68909 128.1976 15
для меня векторизация-это прежде всего упрощение написания и понимания вашего кода.
цель векторизованной функции состоит в том, чтобы устранить Бухгалтерский учет, связанный с циклом for. Например, вместо:
means <- numeric(length(mtcars)) for (i in seq_along(mtcars)) { means[i] <- mean(mtcars[[i]]) } sds <- numeric(length(mtcars)) for (i in seq_along(mtcars)) { sds[i] <- sd(mtcars[[i]]) }
Вы можете написать:
means <- vapply(mtcars, mean, numeric(1)) sds <- vapply(mtcars, sd, numeric(1))
что делает его легче увидеть, что то же самое (входные данные) и что отличается (функция, которую вы применяете).
вторичным преимуществом векторизации является то, что for-loop часто пишется на C, а не на R. Это имеет существенные преимущества в производительности, но я не думаю, что это ключевое свойство векторизации. Векторизация в основном касается сохранения вашего мозга, а не сохранения работы компьютера.
я согласен с мнением Патрика Бернса, что это скорее скрытие петли, а не векторизация кода. Вот почему:
считайте это
C
фрагмент кода:for (int i=0; i<n; i++) c[i] = a[i] + b[i]
что мы хотели бы сделать это совершенно ясно. Но как задача выполнена или как она может быть выполнена на самом деле нет. А for-loop по умолчанию является серийный конструкции. Он не сообщает, Если или как вещи могут быть сделаны в параллельный.
наиболее очевидным способом является то, что код выполняется в последовательно. Загрузить
a[i]
иb[i]
на регистры, добавьте их, сохраните результат вc[i]
, и сделать это для каждогоi
.однако, современные процессоры имеют вектор или SIMD набор инструкций, который способен работать на вектор данных во время та же инструкция при выполнении та же операция (например, добавление двух векторов, как показано выше). В зависимости от процессора / архитектуры, можно было бы добавить, скажем, четыре числа из
a
иb
по той же инструкции, а не по одному.мы хотели бы использовать Одна Инструкция, Множество Данных и выполнить параллелизм уровня данных, т. е. загружать 4 вещи одновременно, добавлять 4 вещи одновременно, хранить 4 вещи одновременно, например. А это векторизация кода.
обратите внимание, что это отличается от распараллеливания кода-где несколько вычислений выполняются одновременно.
было бы здорово, если бы компилятор идентифицировал такие блоки кода и автоматически векторизует их, что является сложной задачей. автоматическая векторизация кода является сложной темой исследования в области компьютерных наук. Но за время компиляторы стали лучше. Вы можете проверить автоматическая векторизация возможности
GNU-gcc
здесь. Аналогично дляLLVM-clang
здесь. И вы также можете найти некоторые тесты в последней ссылке по сравнению сgcc
иICC
(компилятор Intel C++).
gcc
(яv4.9
) например не векторизует код автоматически при-O2
оптимизация уровня. Поэтому, если бы мы должны были выполнить код, показанный выше, это было бы выполняются последовательно. Вот время для добавления двух целочисленных векторов длиной 500 миллионов.нам либо нужно добавить флаг
-ftree-vectorize
или изменить оптимизацию на уровень-O3
. (Обратите внимание, что-O3
выполняет другие дополнительные оптимизации как хорошо). Флаг-fopt-info-vec
полезно, поскольку он сообщает, когда цикл был успешно векторизован).# compiling with -O2, -ftree-vectorize and -fopt-info-vec # test.c:32:5: note: loop vectorized # test.c:32:5: note: loop versioned for vectorization because of possible aliasing # test.c:32:5: note: loop peeled for vectorization to enhance alignment
это говорит нам, что функция векторизированный. Вот тайминги, сравнивающие оба не векторизированный и векторизированный версии на целочисленных векторов длины 500 млн.:
x = sample(100L, 500e6L, TRUE) y = sample(100L, 500e6L, TRUE) z = vector("integer", 500e6L) # result vector # non-vectorised, -O2 system.time(.Call("Csum", x, y, z)) # user system elapsed # 1.830 0.009 1.852 # vectorised using flags shown above at -O2 system.time(.Call("Csum", x, y, z)) # user system elapsed # 0.361 0.001 0.362 # both results are checked for identicalness, returns TRUE
эту часть можно смело пропустить без потери непрерывности.
компиляторы не всегда будут иметь достаточно информации для векторизации. Мы могли бы использовать спецификация OpenMP для параллельного программирования и simd директива компилятора для указания компиляторам векторизовать код. Важно обеспечить, чтобы их не было перекрывает память, состояние гонки и т. д.. при векторизации кода вручную, иначе это приведет к неверным результатам.
#pragma omp simd for (i=0; i<n; i++) c[i] = a[i] + b[i]
делая это, мы специально просим компилятор векторизовать его, несмотря ни на что. Нам нужно будет активировать расширения OpenMP с помощью флага времени компиляции
-fopenmp
. Делая это:# timing with -O2 + OpenMP with simd x = sample(100L, 500e6L, TRUE) y = sample(100L, 500e6L, TRUE) z = vector("integer", 500e6L) # result vector system.time(.Call("Cvecsum", x, y, z)) # user system elapsed # 0.360 0.001 0.360
это здорово! Это было протестировано с помощью gcc v6. 2. 0 и llvm clang v3.9. 0 (оба установлены через homebrew, MacOS 10.12.3), оба из которых поддержка OpenMP 4.0.
в этом смысле, хотя страница Википедии о программировании массивов упоминает, что языки, которые работают на целых массивах, обычно называют это как векторизированный операции, это действительно скрытие петли IMO (если он фактически не векторизован).
в случае R, даже
rowSums()
илиcolSums()
код в C не использовать векторизация кода IIUC; это просто цикл в C. То же самое касаетсяlapply()
. В случаеapply()
, это в R. поэтому все они скрытие петли.короче говоря, обертывание функции R:
просто писать for-loop in
C
!= векторизация кода.
просто пишу for-loop inR
!= векторизация кода.Intel Math Kernel Library (MKL) например реализует векторизованные формы функции.
HTH
ссылки:
- разговор Джеймса Рейндерса, Intel (этот ответ в основном является попыткой резюмировать этот отличный разговор)
Итак, чтобы суммировать отличные ответы / комментарии в какой-то общий ответ и предоставить некоторый фон: R имеет 4 типа циклов (с не-векторизация на заказ векторизация)
- R
for
цикл, который повторно вызывает функции R в каждой итерации (не векторизированный)- цикл C, который повторно вызывает функции R в каждой итерации (не векторизированный)
- цикл C, который вызывает функцию R только один раз (несколько векторизованных)
- простой цикл C, который не вызывает любой R функция вообще и использует ее собственные скомпилированные функции (векторизовать)
так
*apply
семья-это второй тип. Кромеapply
который больше из первого типавы можете понять это из комментария в его исходный код
/* .Внутренний (lapply (X, FUN)) * /
/* это особый .Внутренние, так что имеет неоценимые аргументы. Это
вызывается из обертки закрытия, поэтому X и FUN-это обещания. Веселье должно быть быть недооцененным для использования, например, в bquote . * /что это значит
lapply
код s C принимает не оцененную функцию от R и позже оценивает ее в самом коде C. Это в основном разница междуlapply
s.Internal
вызов.Internal(lapply(X, FUN))
С