Apakah keluarga "* apply" benar-benar tidak vektorisasi?

138

Jadi kami biasa mengatakan kepada setiap R pengguna baru bahwa " applytidak vektor, lihat Patrick Burns R Inferno Circle 4 " yang mengatakan (saya kutip):

Refleks yang umum adalah menggunakan fungsi dalam keluarga terapan. Ini bukan vektorisasi, ini menyembunyikan loop . Fungsi apply memiliki perulangan for dalam definisinya. Fungsi lapply mengubur loop, tetapi waktu eksekusi cenderung kurang lebih sama dengan for loop eksplisit.

Memang, sekilas applykode sumber mengungkapkan loop:

grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] "        for (i in 1L:d2) {"  "    else for (i in 1L:d2) {"

Oke sejauh ini, tapi lihat lapplyatau vapplysebenarnya ungkapkan gambaran yang sama sekali berbeda:

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>

Jadi ternyata tidak ada R forloop yang bersembunyi di sana, melainkan mereka memanggil fungsi tertulis C internal.

Pengamatan sekilas ke dalam lubang kelinci mengungkapkan gambaran yang hampir sama

Selain itu, mari kita ambil colMeansfungsinya misalnya, yang tidak pernah dituduh tidak vektorisasi

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>

Hah? Itu juga hanya panggilan .Internal(colMeans(...yang juga bisa kita temukan di lubang kelinci . Jadi apa bedanya dengan ini .Internal(lapply(..?

Sebenarnya benchmark cepat mengungkapkan bahwa sapplyperformanya tidak lebih buruk dari colMeansdan jauh lebih baik daripada forloop untuk kumpulan data besar

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 

Dengan kata lain, apakah benar untuk mengatakan bahwa lapplydan vapply benar-benar vectorised (dibandingkan dengan applyyang merupakan forlingkaran yang juga panggilan lapply) dan apa yang Patrick Luka bakar benar-benar berarti untuk mengatakan?

David Arenburg
sumber
8
Ini semua ada dalam semantik, tetapi saya tidak akan menganggapnya vektor. Saya menganggap pendekatan vektorisasi jika fungsi R dipanggil hanya sekali dan dapat melewati nilai vektor. *applyfungsi berulang kali memanggil fungsi R, yang membuatnya berputar. Mengenai kinerja yang baik dari sapply(m, mean): Mungkin kode-C lapplyapakah metode hanya mengirimkan sekali dan kemudian memanggil metode itu berulang kali? mean.defaultcukup dioptimalkan.
Roland
4
Pertanyaan bagus, dan terima kasih telah memeriksa kode yang mendasarinya. Saya mencari apakah itu baru saja diubah, tetapi tidak ada tentang ini di catatan rilis R dari versi 2.13.0 dan seterusnya.
ilir
1
Sampai sejauh mana performa bergantung pada platform dan compiler C serta flag linker yang digunakan?
smci
3
@DavidArenburg Sebenarnya, menurut saya ini tidak didefinisikan dengan baik. Setidaknya saya tidak tahu referensi kanonik. Definisi bahasa menyebutkan operasi "vektorisasi", tetapi tidak mendefinisikan vektorisasi.
Roland
3
Sangat terkait: Apakah keluarga terapan R lebih dari gula sintaksis? (Dan, seperti jawaban ini, juga bacaan yang bagus.)
Gregor Thomas

Jawaban:

73

Pertama-tama, dalam contoh Anda, Anda melakukan pengujian pada "data.frame" yang tidak adil colMeans, applydan "[.data.frame"karena memiliki overhead:

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

Pada matriks, gambarnya sedikit berbeda:

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

Mengembalikan bagian utama dari pertanyaan, perbedaan utama antara lapply/ mapply/ etc dan R-loop langsung adalah tempat perulangan dilakukan. Seperti yang dicatat Roland, loop C dan R perlu mengevaluasi fungsi R di setiap iterasi yang paling mahal. Fungsi C yang sangat cepat adalah yang melakukan segala sesuatu di C, jadi, saya rasa, seharusnya ini yang dimaksud dengan "vektorisasi"?

Contoh di mana kita menemukan mean di setiap elemen "daftar":

( EDIT 11 Mei '16 : Saya percaya contoh dengan menemukan "mean" bukanlah penyiapan yang baik untuk perbedaan antara mengevaluasi fungsi R secara berulang dan kode yang dikompilasi, (1) karena kekhususan algoritme mean R pada "numerik" s lebih sederhana sum(x) / length(x)dan (2) seharusnya lebih masuk akal untuk menguji pada "list" dengan length(x) >> lengths(x). Jadi, contoh "mean" dipindahkan ke akhir dan diganti dengan yang lain.)

Sebagai contoh sederhana, kita dapat mempertimbangkan penemuan kebalikan dari setiap length == 1elemen "daftar":

Dalam sebuah tmp.cfile:

#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);
}

Dan di sisi R:

system("R CMD SHLIB /home/~/tmp.c")
dyn.load("/home/~/tmp.so")

dengan data:

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)
})

Pembandingan:

#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

(Mengikuti contoh asli dari mean temuan):

#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
alexis_laz
sumber
10
Poin bagus tentang biaya mengonversi data.frame ke matriks, dan terima kasih telah menyediakan tolok ukur.
Joshua Ulrich
Itu adalah jawaban yang sangat bagus, meskipun saya tidak dapat mengkompilasi fungsi all_Cdan Anda C_and_R. Saya juga ditemukan di dokumentasi dari compiler::cmpfunsebuah versi R lama lapply yang berisi R yang sebenarnya forlingkaran, aku mulai curiga bahwa Burns mengacu bahwa versi lama yang vectorised sejak itu dan ini adalah jawaban yang sebenarnya untuk pertanyaan saya .. ..
David Arenburg
@DavidArenburg: Tolok ukur la1dari ?compiler::cmpfuntampaknya, masih, menghasilkan efisiensi yang sama dengan semua kecuali all_Cfungsi. Saya kira, itu-memang- menjadi masalah definisi; adalah "vektorisasi" yang berarti fungsi apa pun yang menerima tidak hanya skalar, fungsi apa pun yang memiliki kode C, fungsi apa pun yang menggunakan komputasi dalam C saja?
alexis_laz
1
Saya kira semua fungsi di R memiliki kode C di dalamnya, hanya karena semua yang ada di R adalah fungsi (yang harus ditulis dalam beberapa bahasa). Jadi pada dasarnya, jika saya memahaminya dengan benar, Anda mengatakan bahwa lapplytidak vektorisasi hanya karena mengevaluasi fungsi R di setiap iterasi dengan kode C-nya?
David Arenburg
5
@DavidArenburg: Jika saya harus mendefinisikan "vektorisasi" dalam beberapa cara, saya kira, saya akan memilih pendekatan linguistik; yaitu sebuah fungsi yang menerima dan mengetahui bagaimana menangani sebuah "vektor", apakah itu cepat, lambat, ditulis dalam C, dalam R atau apapun. Dalam R, pentingnya vektorisasi adalah karena banyak fungsi yang ditulis dalam C dan menangani vektor, sedangkan dalam bahasa lain, pengguna biasanya akan mengulang input untuk -mendapatkan mean. Itu membuat vektorisasi berhubungan, secara tidak langsung, dengan kecepatan, efisiensi, keamanan, dan ketahanan.
alexis_laz
65

Bagi saya, vektorisasi terutama tentang membuat kode Anda lebih mudah ditulis dan lebih mudah dipahami.

Tujuan dari fungsi vektorisasi adalah untuk menghilangkan pembukuan yang terkait dengan loop for. Misalnya, alih-alih:

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]])
}

Kamu bisa menulis:

means <- vapply(mtcars, mean, numeric(1))
sds   <- vapply(mtcars, sd, numeric(1))

Itu membuatnya lebih mudah untuk melihat apa yang sama (data input) dan apa yang berbeda (fungsi yang Anda terapkan).

Keuntungan sekunder dari vektorisasi adalah bahwa for-loop sering ditulis dalam C, bukan dalam R. Ini memiliki manfaat kinerja yang substansial, tetapi menurut saya itu bukan properti utama vektorisasi. Vektorisasi pada dasarnya adalah tentang menyelamatkan otak Anda, bukan menyelamatkan pekerjaan komputer.

hadley
sumber
5
Saya rasa tidak ada perbedaan performa yang berarti antara forloop C dan R. Oke, loop C mungkin dioptimalkan oleh kompiler, tetapi poin utama kinerja adalah apakah konten loop itu efisien. Dan jelas kode yang dikompilasi biasanya lebih cepat daripada kode yang diinterpretasikan. Tapi mungkin itu yang ingin Anda katakan.
Roland
3
@Roland ya, ini bukan perulangan for itu sendiri, itu semua hal di sekitarnya (biaya panggilan fungsi, kemampuan untuk memodifikasi di tempat, ...).
hadley
10
@DavidArenburg "Konsistensi yang tidak perlu adalah hobgoblin pikiran kecil";)
hadley
6
Tidak, menurut saya kinerja bukanlah poin utama dari memvektor kode Anda. Menulis ulang loop menjadi lapply bermanfaat meskipun tidak lebih cepat. Poin utama dari dplyr adalah membuatnya lebih mudah untuk mengekspresikan manipulasi data (dan sangat bagus juga cepat).
hadley
12
@DavidArenburg itu karena Anda adalah pengguna R. Sebagian besar pengguna baru menganggap loop jauh lebih alami, dan perlu didorong untuk melakukan vektorisasi. Bagi saya, menggunakan fungsi seperti colMeans belum tentu tentang vektorisasi, ini tentang menggunakan kembali kode cepat yang telah ditulis seseorang
hadley
49

Saya setuju dengan pandangan Patrick Burns bahwa ini agak menyembunyikan loop dan bukan vektorisasi kode . Inilah alasannya:

Pertimbangkan Ccuplikan kode ini :

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

Apa yang ingin kami lakukan cukup jelas. Tapi bagaimana tugas itu dilakukan atau bagaimana itu bisa dilakukan sebenarnya tidak. Sebuah for-loop secara default adalah konstruksi serial. Itu tidak menginformasikan apakah atau bagaimana sesuatu dapat dilakukan secara paralel.

Cara yang paling jelas adalah kode dijalankan secara berurutan . Muat a[i]dan b[i]ke register, tambahkan, simpan hasilnya c[i], dan lakukan ini untuk masing-masing i.

Namun, prosesor modern memiliki set instruksi vektor atau SIMD yang mampu beroperasi pada vektor data selama instruksi yang sama ketika melakukan operasi yang sama (misalnya, menambahkan dua vektor seperti yang ditunjukkan di atas). Bergantung pada prosesor / arsitektur, dimungkinkan untuk menambahkan, katakanlah, empat angka dari adan di bbawah instruksi yang sama, alih-alih satu per satu.

Kami ingin mengeksploitasi Single Instruction Multiple Data dan melakukan paralelisme level data , misalnya, memuat 4 hal pada satu waktu, menambahkan 4 hal sekaligus, menyimpan 4 hal sekaligus, misalnya. Dan ini adalah vektorisasi kode .

Perhatikan bahwa ini berbeda dari kode parallelisasi - di mana beberapa komputasi dilakukan secara bersamaan.

Akan sangat bagus jika kompilator mengidentifikasi blok kode seperti itu dan secara otomatis memvektoralnya, yang merupakan tugas yang sulit. Vektorisasi kode otomatis adalah topik penelitian yang menantang dalam Ilmu Komputer. Namun seiring waktu, penyusun menjadi lebih baik dalam hal itu. Anda dapat memeriksa kemampuan vektorisasi otomatis di GNU-gcc sini . Begitu pula untuk di LLVM-clang sini . Dan Anda juga dapat menemukan beberapa tolok ukur di tautan terakhir dibandingkan dengan gccdan ICC(kompiler Intel C ++).

gcc(Saya aktif v4.9) misalnya tidak melakukan vektorisasi kode secara otomatis pada -O2pengoptimalan level. Jadi jika kita mengeksekusi kode yang ditunjukkan di atas, itu akan dijalankan secara berurutan. Berikut waktu untuk menambahkan dua vektor bilangan bulat dengan panjang 500 juta.

Kita perlu menambahkan bendera -ftree-vectorizeatau mengubah pengoptimalan ke level -O3. (Perhatikan bahwa -O3melakukan pengoptimalan tambahan lainnya juga). Flag -fopt-info-vecini berguna karena menginformasikan ketika sebuah loop berhasil di-vectorisasi).

# 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    

Ini memberi tahu kita bahwa fungsi tersebut vektorisasi. Berikut adalah pengaturan waktu yang membandingkan versi non-vektorisasi dan vektorisasi pada vektor integer dengan panjang 500 juta:

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

Bagian ini dapat dilewati dengan aman tanpa kehilangan kontinuitas.

Penyusun tidak akan selalu memiliki informasi yang cukup untuk melakukan vektorisasi. Kita bisa menggunakan spesifikasi OpenMP untuk pemrograman paralel , yang juga menyediakan arahan kompiler simd untuk menginstruksikan kompiler untuk membuat vektor kode. Sangat penting untuk memastikan bahwa tidak ada memori yang tumpang tindih, kondisi balapan, dll .. saat melakukan vektorisasi kode secara manual, jika tidak maka akan menghasilkan hasil yang salah.

#pragma omp simd
for (i=0; i<n; i++) 
  c[i] = a[i] + b[i]

Dengan melakukan ini, kami secara khusus meminta kompilator untuk melakukan vektorisasi apa pun yang terjadi. Kita perlu mengaktifkan ekstensi OpenMP dengan menggunakan bendera waktu kompilasi -fopenmp. Dengan melakukan itu:

# 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

yang bagus! Ini telah diuji dengan gcc v6.2.0 dan llvm clang v3.9.0 (keduanya diinstal melalui homebrew, MacOS 10.12.3), keduanya mendukung OpenMP 4.0.


Dalam hal ini, meskipun halaman Wikipedia di Array Programming menyebutkan bahwa bahasa yang beroperasi di seluruh array biasanya menyebutnya sebagai operasi vektor , itu sebenarnya adalah IMO yang menyembunyikan loop (kecuali jika benar-benar vektorisasi).

Dalam kasus R, genap rowSums()atau colSums()kode dalam C jangan memanfaatkan vektorisasi kode IIUC; itu hanya satu loop di C. Hal yang sama berlaku untuk lapply(). Dalam kasus apply(), itu dalam R. Semua ini karena itu bersembunyi loop .

Singkatnya, membungkus fungsi R dengan:

hanya menulis for-loop in C! = memvektor kode Anda.
hanya menulis for-loop in R! = memvektor kode Anda.

Intel Math Kernel Library (MKL) misalnya mengimplementasikan bentuk fungsi vektor.

HTH


Referensi:

  1. Bicara oleh James Reinders, Intel (jawaban ini sebagian besar merupakan upaya untuk meringkas pembicaraan yang luar biasa ini)
Arun
sumber
35

Jadi untuk menjumlahkan jawaban / komentar hebat menjadi beberapa jawaban umum dan memberikan beberapa latar belakang: R memiliki 4 jenis loop ( dari urutan tidak-vektorisasi ke vektor )

  1. forLoop R yang berulang kali memanggil fungsi R di setiap iterasi ( Tidak vektorisasi )
  2. Loop C yang berulang kali memanggil fungsi R di setiap iterasi ( Tidak vektorisasi )
  3. C loop yang memanggil fungsi R hanya sekali ( Agak vektorisasi )
  4. Sebuah C lingkaran polos yang tidak memanggil setiap fungsi R sama sekali dan menggunakannya memiliki fungsi dikompilasi ( vektoralisasi )

Jadi *applykeluarga adalah tipe kedua. Kecuali applyyang lebih dari tipe pertama

Anda dapat memahami ini dari komentar di kode sumbernya

/ *. Internal (lapply (X, FUN)) * /

/ * Ini adalah .Internal khusus, sehingga memiliki argumen yang tidak dievaluasi. Ini
dipanggil dari pembungkus penutupan, jadi X dan FUN adalah janji. FUN harus dievaluasi untuk digunakan misalnya dalam bquote. * /

Itu berarti bahwa lapplykode C menerima fungsi yang tidak dievaluasi dari R dan kemudian mengevaluasinya di dalam kode C itu sendiri. Ini pada dasarnya adalah perbedaan antara panggilan lapplys.Internal

.Internal(lapply(X, FUN))

Yang memiliki FUNargumen yang memegang fungsi R.

Dan colMeans .Internalpanggilan yang tidak memiliki FUNargumen

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

colMeans, tidak seperti lapplytahu persis fungsi apa yang perlu digunakan, sehingga menghitung mean secara internal dalam kode C.

Anda dapat dengan jelas melihat proses evaluasi fungsi R di setiap iterasi dalam lapplykode 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);
   }

Singkatnya, lapplytidak vektorisasi , meskipun memiliki dua keuntungan yang mungkin dibandingkan forloop R biasa

  1. Mengakses dan menugaskan dalam satu lingkaran tampaknya lebih cepat di C (yaitu dalam lapplymemasukkan fungsi) Meskipun perbedaannya tampak besar, kami tetap berada di tingkat mikrodetik dan yang mahal adalah penilaian fungsi R di setiap iterasi. Contoh sederhana:

    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. Seperti yang disebutkan oleh @Roland, ini menjalankan loop C terkompilasi, bukan loop R yang ditafsirkan


Meskipun saat memvektor kode Anda, ada beberapa hal yang perlu Anda pertimbangkan.

  1. Jika Anda set data (panggilan mari itu df) adalah dari kelas data.frame, beberapa fungsi Vectorized (seperti colMeans, colSums, rowSums, dll) harus mengubahnya menjadi sebuah matriks pertama, hanya karena ini adalah bagaimana mereka dirancang. Artinya untuk yang besar dfini bisa membuat overhead yang besar. Sementara lapplytidak perlu melakukan ini karena mengekstrak vektor sebenarnya dari df(seperti data.framehanya daftar vektor) dan dengan demikian, jika Anda tidak memiliki banyak kolom tetapi banyak baris, lapply(df, mean)terkadang bisa menjadi pilihan yang lebih baik daripada colMeans(df).
  2. Hal lain yang perlu diingat adalah bahwa R memiliki banyak variasi tipe fungsi yang berbeda, seperti .Primitive, dan generic ( S3, S4) lihat di sini untuk beberapa informasi tambahan. Fungsi generik harus melakukan pengiriman metode yang terkadang membutuhkan biaya yang mahal. Misalnya, meanadalah S3fungsi generik while sumis Primitive. Jadi beberapa kali lapply(df, sum)bisa sangat efisien dibandingkan colSumsdengan alasan yang disebutkan di atas
David Arenburg
sumber
1
Ringkasan yang sangat kohesif. Hanya beberapa catatan: (1) C tahu bagaimana menangani "data.frame", karena mereka adalah "list" dengan atribut; itu colMeansdll yang dibangun untuk menangani hanya matriks. (2) Saya agak bingung dengan kategori ketiga Anda; Saya tidak tahu apa -exaclty- yang Anda maksud. (3) Karena Anda mengacu secara khusus lapply, saya yakin tidak ada bedanya antara "[<-"di R dan C; mereka berdua mengalokasikan "daftar" (SEXP) sebelumnya dan mengisinya di setiap iterasi ( SET_VECTOR_ELTdi C), kecuali saya kehilangan maksud Anda.
alexis_laz
2
Saya mengerti maksud Anda do.callbahwa ia membangun panggilan fungsi di lingkungan C dan hanya mengevaluasinya; meskipun saya mengalami kesulitan untuk membandingkannya dengan perulangan atau vektorisasi karena itu melakukan hal yang berbeda. Anda, sebenarnya, benar tentang mengakses dan menetapkan perbedaan antara C dan R, meskipun keduanya tetap pada tingkat mikrodetik dan tidak mempengaruhi hasil hasil yang sangat besar, karena yang mahal adalah panggilan fungsi R berulang (bandingkan R_loopdan R_lapplydalam jawaban saya ). (Saya akan mengedit posting Anda dengan patokan; Saya harap Anda, tetap saja, tidak keberatan)
alexis_laz
2
Saya tidak mencoba untuk tidak setuju - dan saya bingung, sejujurnya, tentang apa yang tidak Anda setujui. Komentar saya sebelumnya bisa dikatakan lebih baik. Saya mencoba memperbaiki terminologi yang digunakan karena istilah "vektorisasi" memiliki dua definisi yang sering digabungkan. Saya tidak berpikir ini bisa diperdebatkan. Burns dan Anda tampaknya ingin menggunakannya hanya dalam arti implementasi, tetapi Hadley dan banyak anggota R-Core (mengambil Vectorize()sebagai contoh) menggunakannya juga dalam pengertian UI. Saya pikir banyak ketidaksepakatan di utas ini disebabkan oleh penggunaan satu istilah untuk dua konsep yang terpisah tetapi terkait.
Gregor Thomas
3
@DavidArenburg dan bukankah itu vektorisasi dalam arti UI, terlepas dari apakah ada perulangan for di R atau C di bawahnya?
Gregor Thomas
2
@DavidArenburg, Gregor, menurut saya ada kebingungan antara "vektorisasi kode" dan "fungsi vektorisasi". Di R, penggunaan tampaknya cenderung ke arah yang terakhir. "Vektorisasi kode" menjelaskan pengoperasian pada vektor dengan panjang 'k' dalam instruksi yang sama. Membungkus fn. di sekitar hasil kode gila dalam "fungsi vektorisasi" (ya, itu tidak masuk akal dan membingungkan, saya setuju, lebih baik akan menyembunyikan loop atau fungsi i / p vektor ) dan tidak perlu ada hubungannya dengan kode vektorisasi . Di R, apply akan menjadi fungsi vektorisasi , tetapi fungsi ini tidak memvektorisasi kode Anda, melainkan beroperasi pada vektor.
Arun