Чи справді сім'я «застосувати» не векторизована?


138

Тож ми звикли говорити кожному новому користувачеві R, що " applyне векторизовано, перевірте Пакет Парк Бернс R Inferno Circle 4 ", який говорить (цитую):

Поширений рефлекс - використання функції в сімействі застосувань. Це не векторизація, це приховування циклу . У застосуванні функція має цикл for для визначення. Функція lapply закопує цикл, але час виконання, як правило, приблизно дорівнює явному циклу.

Дійсно, швидкий погляд на 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>

Тож, мабуть, там не forховається цикл R , скоріше вони викликають внутрішню функцію C.

Швидкий погляд у кролячу нору виявляє майже таку ж картину

Більше того, візьмемо 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 насправді vectorised ( по порівнянні з applyякої є forцикл , який також називає lapply) , і що ж Патрік Бернс дійсно хотів сказати?


8
Це все в семантиці, але я б не вважав їх векторизованими. Я вважаю підхід векторизованим, якщо функція R викликається лише один раз і може передаватися вектором значень. *applyфункції багаторазово викликають R-функції, що робить їх петлями. Що стосується хорошої продуктивності sapply(m, mean): Можливо, C-код lapplyметоду розсилає лише один раз, а потім викликає метод повторно? mean.defaultдосить оптимізований.
Роланд

4
Відмінне запитання та спасибі за перевірку основного коду. Я шукав, чи було нещодавно змінено, але нічого про це у примітках до випуску R від версії 2.13.0 далі.
ilir

1
Наскільки продуктивність залежить як від платформи, так і від C-компілятора та прапорців Linker?
smci

3
@DavidArenburg Насправді, я не думаю, що він є чітко визначеним. Принаймні, я не знаю канонічного посилання. Визначення мови згадує "векторизовані" операції, але не визначає векторизацію.
Роланд

3
Дуже пов’язано: чи сім'я R застосовується більше ніж синтаксичний цукор? (І, як ці відповіді, також добре прочитайте.)
Грегор Томас

Відповіді:


73

Перш за все, у вашому прикладі ви робите тести на "data.frame", що не справедливо 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/ і т. Д. Та прямих R-циклів полягає в тому, де робиться циклічне циклічне завершення. Як зазначає Роланд, і для циклів C і R необхідно оцінювати функцію R у кожній ітерації, яка є найдорожчою. Дійсно швидкі функції C - це ті, що роблять все в C, так що, я думаю, саме це має бути "векторизованим"?

Приклад, коли ми знаходимо середнє значення в кожному з елементів «списку»:

( EDIT 11 травня 1616 : Я вважаю, що приклад із знаходженням "середнього" не є хорошим параметром для відмінностей між ітераційним оцінкою функції R та компільованим кодом (1) через особливості середнього алгоритму R на "числовому" s над простим sum(x) / length(x)і (2) слід мати більше сенсу перевіряти на "list" s length(x) >> lengths(x). Отже, "середній" приклад переміщується до кінця і замінюється іншим.)

Як простий приклад ми могли б розглянути знаходження протилежної кожного length == 1елемента "списку":

У tmp.cфайлі:

#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

10
Відмінний момент щодо витрат на перетворення data.frame в матрицю та спасибі за надання орієнтирів.
Джошуа Ульріх

Це дуже приємна відповідь, хоча я не зміг скласти ваші all_Cта C_and_Rфункції. Я також знайшов в документації про compiler::cmpfunякість старої версії R lapply , який містить фактичний R forцикл, я починаю підозрювати , що Бернс мав в виду , що стара версія , яка була vectorised з тих пір і це реальна відповідь на моє запитання .. ..
Девід Аренбург

@DavidArenburg: Бенчмаркінг la1з, ?compiler::cmpfunздається, все-таки, щоб досягти однакової ефективності з усіма, крім all_Cфункцій. Я здогадуюсь, це -indeed- стає питанням визначення; "векторизована" означає будь-яку функцію, яка приймає не тільки скаляри, будь-яку функцію, яка має код C, будь-яку функцію, яка використовує обчислення лише в C?
alexis_laz

1
Я здогадуюсь, що всі функції в R мають код C у них, просто тому, що все в R - це функція (яку потрібно було писати якоюсь мовою). Отже, якщо я правильно це розумію, ви говорите, що lapplyце не векторизовано просто тому, що він оцінює функцію R у кожній ітерації за кодом С?
Девід Аренбург

5
@DavidArenburg: Якщо я мушу якось визначити "векторизацію", то, мабуть, я вибрав би мовний підхід; тобто функція, яка приймає і вміє поводитися з "вектором", будь то швидкий, повільний, написаний на C, в R або що-небудь інше. У R важливість векторизації полягає в тому, що багато функцій записані в С та обробляють вектори, тоді як в інших мовах користувачі, як правило, переходять на вхід, щоб знайти середнє значення. Це змушує векторизацію співпрацювати, опосередковано, зі швидкістю, ефективністю, безпекою та надійністю.
alexis_laz

65

Для мене векторизація - це перш за все полегшення запису коду та його легше розуміння.

Метою векторизованої функції є усунення бухгалтерського обліку, пов'язаного з циклом 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 часто пишеться на C, а не в R. Це має значні переваги від продуктивності, але я не думаю, що це ключова властивість векторизації. Векторизація принципово стосується збереження вашого мозку, а не економії роботи на комп’ютері.


5
Я не думаю, що між forциклами C і R є змістовна різниця в продуктивності . Гаразд, компілятор може оптимізувати цикл С, але головним моментом для ефективності є те, чи ефективний вміст циклу. І очевидно, що компільований код зазвичай швидший, ніж інтерпретований код. Але це, мабуть, те, що ти мав намір сказати.
Роланд

3
@Roland так, це не сам цикл for-per, але все це навколо нього (вартість виклику функції, можливість зміни на місці, ...).
Хадлі

10
@DavidArenburg "Непотрібна послідовність - це гобгоблін малих розумів";)
hadley

6
Ні, я не думаю, що ефективність є головним моментом векторизації вашого коду. Перезапис циклу в лаптоп вигідний, навіть якщо це не швидше. Основним моментом dplyr є те, що він полегшує висловлення маніпуляцій з даними (і просто дуже приємно, що це також швидко).
Хадлі

12
@DavidArenburg це тому, що ви досвідчений користувач R. Більшість нових користувачів вважають петлі набагато природнішими, і їх потрібно заохочувати векторизувати. Для мене використання такої функції, як colMeans, не обов'язково стосується векторизації, це повторне використання швидкого коду, який хтось уже написав
hadley

49

Я погоджуюся з думкою Патріка Бернса, що це швидше приховування циклу, а не векторизація коду . Ось чому:

Розглянемо цей Cфрагмент коду:

for (int i=0; i<n; i++)
  c[i] = a[i] + b[i]

Що ми хотіли б зробити, цілком зрозуміло. Але те, як виконується завдання або як воно може бути виконане, насправді не так. Для контуру за замовчуванням є серійної конструкцією. Він не повідомляє, чи можна робити паралельно.

Найбільш очевидний спосіб - це те, що код запускається послідовно . Завантажте 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.


У цьому сенсі, навіть незважаючи на те, що сторінка Вікіпедії в програмуванні масиву згадує, що мови, які працюють на цілих масивах, зазвичай називають це векторизованими операціями , це дійсно за циклами, що приховують ІМО (якщо він насправді не векторизований).

У разі R, парного rowSums()чи colSums()коду в C не використовувати векторизацію коду IIUC; це просто цикл у C. Те саме стосується lapply(). У випадку apply(), якщо це в Р. Все це переховується в петлі .

Коротше кажучи, завершення функції R за допомогою:

просто писати для циклу в C! = vectorising код.
просто писати для циклу в R! = vectorising код.

Наприклад, бібліотека ядер Intel Math (MKL) реалізує векторизовані форми функцій.

HTH


Список літератури:

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

35

Отже, підсумовуйте великі відповіді / коментарі до загальної відповіді та надайте деяку інформацію: R має 4 типи циклів ( від невекторизованого до векторизованого порядку )

  1. forЦикл R, який неодноразово викликає функції R у кожній ітерації ( не векторний )
  2. Цикл C, який неодноразово викликає функції R у кожній ітерації ( не векторний )
  3. Цикл C, який викликає функцію R лише один раз ( Дещо векторизований )
  4. Простий цикл C, який взагалі не викликає жодної функції R і використовує власні компільовані функції ( векторизовані )

Тож *applyсім'я другого типу. За винятком того, applyщо більше першого типу

Це можна зрозуміти з коментаря у його вихідному коді

/ * .Внутрішній (наскрізь (X, FUN)) * /

/ * Це особливий. Внутрішній, тому має неоцінені аргументи. Його
називають із обгорткової оболонки, тому X та FUN - це обіцянки. FUN повинен бути неоціненим для використання, наприклад, у біотех. * /

Це означає, що lapplys C код приймає від R неоціненну функцію і пізніше оцінює її в межах самого коду C. Це в основному різниця між lapplys .Internalвикликом

.Internal(lapply(X, FUN))

Який має FUN аргумент, який містить функцію R

І colMeans .Internalдзвінок, який не має FUNаргументу

.Internal(colMeans(Re(x), n, prod(dn), na.rm))

colMeans, на відміну точноlapply знає що яку функцію йому потрібно використовувати, таким чином, він обчислює внутрішнє значення в коді C.

Ви можете чітко бачити процес оцінки функції R в кожній ітерації в lapplyкоді C

 for(R_xlen_t i = 0; i < n; i++) {
      if (realIndx) REAL(ind)[0] = (double)(i + 1);
      else INTEGER(ind)[0] = (int)(i + 1);
      tmp = eval(R_fcall, rho);   // <----------------------------- here it is
      if (MAYBE_REFERENCED(tmp)) tmp = lazy_duplicate(tmp);
      SET_VECTOR_ELT(ans, i, tmp);
   }

Підводячи підсумок, lapplyце не векторизовано , хоча воно має дві можливі переваги перед простою forпетлею R

  1. Доступ та присвоєння циклу здається швидшим у С (тобто у lapplyфункції) Хоча різниця здається великою, ми все-таки залишаємося на рівні мікросекунди, і дорога річ - це оцінка функції R у кожній ітерації. Простий приклад:

    ffR = function(x)  {
        ans = vector("list", length(x))
        for(i in seq_along(x)) ans[[i]] = x[[i]]
        ans 
    }
    
    ffC = inline::cfunction(sig = c(R_x = "data.frame"), body = '
        SEXP ans;
        PROTECT(ans = allocVector(VECSXP, LENGTH(R_x)));
        for(int i = 0; i < LENGTH(R_x); i++) 
               SET_VECTOR_ELT(ans, i, VECTOR_ELT(R_x, i));
        UNPROTECT(1);
        return(ans); 
    ')
    
    set.seed(007) 
    myls = replicate(1e3, runif(1e3), simplify = FALSE)     
    mydf = as.data.frame(myls)
    
    all.equal(ffR(myls), ffC(myls))
    #[1] TRUE 
    all.equal(ffR(mydf), ffC(mydf))
    #[1] TRUE
    
    microbenchmark::microbenchmark(ffR(myls), ffC(myls), 
                                   ffR(mydf), ffC(mydf),
                                   times = 30)
    #Unit: microseconds
    #      expr       min        lq    median        uq       max neval
    # ffR(myls)  3933.764  3975.076  4073.540  5121.045 32956.580    30
    # ffC(myls)    12.553    12.934    16.695    18.210    19.481    30
    # ffR(mydf) 14799.340 15095.677 15661.889 16129.689 18439.908    30
    # ffC(mydf)    12.599    13.068    15.835    18.402    20.509    30
  2. Як згадував @Roland, він виконує компільований цикл C, а не інтерпретований цикл R


Хоча при векторизації коду є деякі речі, які потрібно враховувати.

  1. Якщо набір даних (виклик ДАВАЙТЕ його df) має клас data.frame, деякі векторизованних функції (такі як colMeans, colSums, rowSumsі т.д.) повинні перетворити його в матрицю перше, просто тому , що це те , як вони були розроблені. Це означає, що для великого dfце може створити величезні накладні витрати. Хоча lapplyцього не доведеться робити, оскільки він витягує фактичні вектори з df(як data.frameце лише список векторів), і, отже, якщо у вас не так багато стовпців, але багато рядків, lapply(df, mean)іноді може бути кращий варіант, ніж colMeans(df).
  2. Ще слід пам’ятати, що R має велику різноманітність різних типів функцій, таких як .Primitive, і generic ( S3, S4), див. Тут для отримання додаткової інформації. Загальна функція повинна виконувати метод відправки, який іноді дорого коштує. Наприклад, meanце родова S3функція, поки sumє Primitive. Таким чином, деякі рази lapply(df, sum)можуть бути дуже ефективними порівняно colSumsз переліченими вище причинами

1
Дуже згуртований підсумок. Лише кілька зауважень: (1) C знає, як обробляти "data.frame" s, оскільки вони "список" з атрибутами; це те, colMeansщо створено для обробки лише матриць. (2) Я трохи заплутаний у вашій третій категорії; Я не можу сказати, на що саме йдеться. (3) Оскільки ви конкретно посилаєтесь lapply, я вважаю, що це не має різниці між "[<-"R і C; вони обидва попередньо виділяють "список" (SEXP) і заповнюють його в кожній ітерації ( SET_VECTOR_ELTна С), якщо я не пропускаю вашу думку.
alexis_laz

2
Я розумію, do.callщо він будує виклик функції в середовищі C і просто оцінює його; хоча мені важко порівняти це циклічне чи векторизаційне, оскільки це робить іншу річ. Насправді ви маєте рацію в доступі та призначенні відмінностей між C і R, хоча обидва залишаються на рівні мікросекунди і не впливають на результат перезавантаження сильно, оскільки дорогий - це ітеративний виклик функції R (порівняйте R_loopі R_lapplyв моїй відповіді ). (Я відредагую ваше повідомлення з орієнтиром; я сподіваюся, ви все одно не заперечуєте)
alexis_laz

2
Я не намагаюся погодитись --- і я, щиро кажучи, плутаю те, з чим ви не згодні. Мій попередній коментар міг би бути сформульований краще. Я намагаюся уточнити використовувану термінологію, оскільки термін "векторизація" має два визначення, які часто є незрозумілими. Я не думаю, що це сперечається. Бернс, і ти, здається, хочеш використовувати його лише в сенсі реалізації, але Хедлі та багато членів R-Core (беручи Vectorize()за приклад) також його використовують у сенсі користувальницького інтерфейсу. Я думаю, що велика частина розбіжностей у цій темі викликана використанням одного терміна для двох окремих понять, але пов'язаних між собою.
Грегор Томас

3
@DavidArenburg чи це не векторизація в сенсі користувальницького інтерфейсу, незалежно від того, чи є цикл for для R або C під ним?
Грегор Томас

2
@DavidArenburg, Грегор, я думаю, що плутанина між "кодовою векторизацією" та "векторизованими функціями". У R, використання здається схильним до останнього. "Векторизація коду" описує роботу над вектором довжини 'k' в цій самій інструкції. Загортання фн. навколо кодового коду призводить до "векторизованих функцій" (так, це не має сенсу і є заплутаним, я погоджуюся, краще було б приховування циклу або векторних i / p функцій ) і не потрібно мати нічого спільного з векторизацією коду . У R застосувати буде векторизована функція , але вона не векторизує ваш код, скоріше працює на векторах.
Арун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.