Действительно ли семейство "* 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 123

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++).

gccv4.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 in R != векторизация кода.

Intel Math Kernel Library (MKL) например реализует векторизованные формы функции.

HTH


ссылки:

  1. разговор Джеймса Рейндерса, Intel (этот ответ в основном является попыткой резюмировать этот отличный разговор)

Итак, чтобы суммировать отличные ответы / комментарии в какой-то общий ответ и предоставить некоторый фон: R имеет 4 типа циклов (с не-векторизация на заказ векторизация)

  1. R for цикл, который повторно вызывает функции R в каждой итерации (не векторизированный)
  2. цикл C, который повторно вызывает функции R в каждой итерации (не векторизированный)
  3. цикл C, который вызывает функцию R только один раз (несколько векторизованных)
  4. простой цикл C, который не вызывает любой R функция вообще и использует ее собственные скомпилированные функции (векторизовать)

так *apply семья-это второй тип. Кроме apply который больше из первого типа

вы можете понять это из комментария в его исходный код

/* .Внутренний (lapply (X, FUN)) * /

/* это особый .Внутренние, так что имеет неоценимые аргументы. Это
вызывается из обертки закрытия, поэтому X и FUN-это обещания. Веселье должно быть быть недооцененным для использования, например, в bquote . * /

что это значит lapplyкод s C принимает не оцененную функцию от R и позже оценивает ее в самом коде C. Это в основном разница между lapplys .Internal вызов

.Internal(lapply(X, FUN))

С