La famiglia "* apply" non è davvero vettorializzata?


138

Quindi siamo abituati a dire a ogni nuovo utente R che " applynon è vettoriale, controlla il Patrick Burns R Inferno Circle 4 " che dice (cito):

Un riflesso comune è usare una funzione nella famiglia di applicazione. Questa non è vettorializzazione, si nasconde in loop . La funzione apply ha un ciclo for nella sua definizione. La funzione lappone seppellisce il loop, ma i tempi di esecuzione tendono ad essere approssimativamente uguali a un esplicito per loop.

In effetti, una rapida occhiata al applycodice sorgente rivela il ciclo:

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

Ok finora, ma uno sguardo lapplyo vapplyrivela effettivamente un'immagine completamente diversa:

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>

Quindi a quanto pare non c'è un forciclo R nascosto lì, piuttosto stanno chiamando la funzione scritta C interna.

Una rapida occhiata al coniglio buco rivela più o meno la stessa immagine

Inoltre, prendiamo colMeansad esempio la funzione, che non è mai stata accusata di non essere vettorializzata

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>

Eh? Chiama anche .Internal(colMeans(...ciò che possiamo anche trovare nella tana del coniglio . Quindi, come è diverso da questo .Internal(lapply(..?

In realtà un rapido benchmark rivela che sapplynon ha prestazioni peggiori colMeanse molto migliori di un forloop per un set di big data

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 

In altre parole, è corretto dirlo lapplye in vapply realtà sono vettorializzati (rispetto a applyquale è un forloop che chiama anche lapply) e cosa intendeva davvero dire Patrick Burns?


8
Questo è tutto nella semantica, ma non li considero vettorializzati. Considero un approccio vettorializzato se una funzione R viene chiamata una sola volta e può essere trasmessa un vettore di valori. *applyLe funzioni richiamano ripetutamente le funzioni R, il che le rende loop. Per quanto riguarda le buone prestazioni di sapply(m, mean): Forse il codice C di lapplymetodo invia solo una volta e quindi chiama ripetutamente il metodo? mean.defaultè abbastanza ottimizzato.
Roland

4
Ottima domanda, e grazie per aver controllato il codice sottostante. Stavo cercando se è stato modificato di recente, ma nulla di tutto ciò nelle note sulla versione R dalla versione 2.13.0 in poi.
ilir,

1
In che misura le prestazioni dipendono sia dalla piattaforma che dai flag del compilatore C e del linker utilizzati?
smci,

3
@DavidArenburg In realtà, non penso che sia ben definito. Almeno non conosco un riferimento canonico. La definizione del linguaggio menziona operazioni "vettorializzate", ma non definisce la vettorializzazione.
Roland

3
Molto correlato: la famiglia R è più dello zucchero sintattico? (E, come queste risposte, anche una buona lettura.)
Gregor Thomas,

Risposte:


73

Prima di tutto, nel tuo esempio si fare i test su un "data.frame", che non è giusto per colMeans, applye "[.data.frame"dal momento che hanno un 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

Su una matrice, l'immagine è leggermente diversa:

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

Riguardo alla parte principale della domanda, la differenza principale tra lapply/ mapply/ etc e i semplici R-loop è dove viene eseguito il loop. Come osserva Roland, entrambi i loop C e R devono valutare una funzione R in ogni iterazione che è la più costosa. Le funzioni C veramente veloci sono quelle che fanno tutto in C, quindi, immagino, questo dovrebbe essere ciò che è "vettorializzato"?

Un esempio in cui troviamo la media in ciascuno degli elementi di un "elenco":

( EDIT 11 maggio 16 : credo che l'esempio nel trovare la "media" non sia una buona impostazione per le differenze tra la valutazione iterativa di una funzione R e il codice compilato, (1) a causa della particolarità dell'algoritmo medio di R su "numerico" s su un semplice sum(x) / length(x)e (2) dovrebbe avere più senso testare su "list" s con length(x) >> lengths(x). Quindi, l'esempio "mean" viene spostato alla fine e sostituito con un altro.)

Come semplice esempio, potremmo considerare la scoperta del contrario di ogni length == 1elemento di un "elenco":

In un 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);
}

E nel lato R:

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

con dati:

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

Analisi comparativa:

#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

(Segue l'esempio originale di ricerca della media):

#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
Un grande punto sui costi di conversione di data.frame in una matrice e grazie per aver fornito benchmark.
Joshua Ulrich,

Questa è una risposta molto bello, anche se non ero in grado di compilare i vostri all_Ce C_and_Rfunzioni. Ho anche trovato nelle documentazioni di compiler::cmpfununa vecchia versione R di lapply che contiene un vero forciclo R , sto iniziando a sospettare che Burns si riferisse a quella vecchia versione che è stata vettorializzata da allora e questa è la vera risposta alla mia domanda. ..
David Arenburg,

@DavidArenburg: il benchmarking la1da ?compiler::cmpfunsembra, comunque, produrre la stessa efficienza con tutte le all_Cfunzioni tranne . Immagino che -indeed- diventi una questione di definizione; "vettorializzare" significa qualsiasi funzione che accetta non solo scalari, qualsiasi funzione che ha il codice C, qualsiasi funzione che utilizza calcoli solo in C?
alexis_laz,

1
Immagino che tutte le funzioni in R contengano il codice C, semplicemente perché tutto in R è una funzione (che doveva essere scritta in un linguaggio). Quindi in pratica, se lo capisco bene, stai dicendo che lapplynon è vettorializzato semplicemente perché sta valutando una funzione R in ogni iterazione con il suo codice C?
David Arenburg,

5
@DavidArenburg: se dovessi definire la "vettorializzazione" in qualche modo, immagino, sceglierei un approccio linguistico; cioè una funzione che accetta e sa come gestire un "vettore", che sia veloce, lento, scritto in C, in R o altro. In R, l'importanza della vettorializzazione sta nel fatto che molte funzioni sono scritte in C e gestiscono i vettori mentre in altre lingue gli utenti, di solito, eseguono il ciclo sull'input per trovare la media. Ciò rende la vettorializzazione in relazione, indirettamente, con velocità, efficienza, sicurezza e robustezza.
alexis_laz,

65

Per me, la vettorializzazione consiste principalmente nel rendere il tuo codice più facile da scrivere e da capire.

L'obiettivo di una funzione vettoriale è eliminare la contabilità associata a un ciclo for. Ad esempio, anziché:

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

Tu puoi scrivere:

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

Ciò rende più semplice vedere quali sono gli stessi (i dati di input) e quelli diversi (la funzione che stai applicando).

Un vantaggio secondario della vettorializzazione è che il for-loop è spesso scritto in C, piuttosto che in R. Questo ha notevoli vantaggi in termini di prestazioni, ma non penso che sia la proprietà chiave della vettorializzazione. La vettorializzazione consiste fondamentalmente nel salvare il tuo cervello, non nel salvare il lavoro del computer.


5
Non credo che ci sia una significativa differenza di prestazioni tra i forloop C e R. OK, un ciclo C potrebbe essere ottimizzato dal compilatore, ma il punto principale per le prestazioni è se il contenuto del ciclo è efficiente. E ovviamente il codice compilato è generalmente più veloce del codice interpretato. Ma è probabilmente quello che volevi dire.
Roland

3
@Roland sì, non è il for-loop in sé, è tutto ciò che lo circonda (il costo di una chiamata di funzione, la possibilità di modificare sul posto, ...).
Hadley,

10
@DavidArenburg "La consistenza inutile è il folletto delle piccole menti";)
Hadley,

6
No, non credo che le prestazioni siano il punto principale nel vettorializzare il tuo codice. Riscrivere un loop in un lapply è utile anche se non è più veloce. Il punto principale di dplyr è che facilita l'espressione della manipolazione dei dati (ed è molto bello che sia anche veloce).
Hadley,

12
@DavidArenburg è perché sei un utente R esperto. La maggior parte dei nuovi utenti trova che i loop sono molto più naturali e devono essere incoraggiati a vettorializzare. Per me, usare una funzione come colMeans non riguarda necessariamente la vettorializzazione, si tratta di riutilizzare il codice veloce che qualcuno ha già scritto
Hadley,

49

Sono d'accordo con l'opinione di Patrick Burns sul fatto che si tratti piuttosto di nascondere il ciclo e non di vettorializzare il codice . Ecco perché:

Considera questo Cframmento di codice:

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

Quello che vorremmo fare è abbastanza chiaro. Ma come l'attività viene eseguita o come potrebbe essere eseguita non è davvero. Un for-loop di default è un costrutto seriale. Non informa se o come le cose possono essere fatte in parallelo.

Il modo più ovvio è che il codice viene eseguito in modo sequenziale . Carica a[i]e b[i]attiva i registri, aggiungili, archivia il risultato c[i]e fallo per ciascuno i.

Tuttavia, i processori moderni hanno un set di istruzioni vettoriali o SIMD che è in grado di operare su un vettore di dati durante la stessa istruzione quando esegue la stessa operazione (ad esempio, aggiungendo due vettori come mostrato sopra). A seconda del processore / architettura, potrebbe essere possibile aggiungere, diciamo, quattro numeri da ae bsotto la stessa istruzione, anziché uno alla volta.

Vorremmo sfruttare i dati multipli di istruzione singola ed eseguire il parallelismo a livello di dati , ad esempio caricare 4 cose alla volta, aggiungere 4 cose alla volta, archiviare 4 cose alla volta, ad esempio. E questa è la vettorializzazione del codice .

Si noti che questo è diverso dalla parallelizzazione del codice, in cui più calcoli vengono eseguiti contemporaneamente.

Sarebbe bello se il compilatore identifichi tali blocchi di codice e li vettorizzi automaticamente , il che è un compito difficile. La vettorializzazione automatica del codice è un argomento di ricerca stimolante in Informatica. Ma nel tempo, i compilatori sono migliorati. Puoi verificare le funzionalità di vettorializzazione automatica di GNU-gcc qui . Allo stesso modo per LLVM-clang qui . E puoi anche trovare alcuni benchmark nell'ultimo link rispetto a gcce ICC(compilatore Intel C ++).

gcc(Sono v4.9attivo), ad esempio, non vettorializza automaticamente il codice -O2all'ottimizzazione del livello. Quindi, se dovessimo eseguire il codice mostrato sopra, verrebbe eseguito in sequenza. Ecco i tempi per l'aggiunta di due vettori interi di lunghezza 500 milioni.

O dobbiamo aggiungere il flag -ftree-vectorizeo modificare l'ottimizzazione al livello -O3. (Nota che -O3esegue anche altre ottimizzazioni aggiuntive ). Il flag -fopt-info-vecè utile in quanto informa quando un loop è stato vettorializzato correttamente).

# 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    

Questo ci dice che la funzione è vettorializzata. Ecco i tempi che confrontano le versioni non vettorializzate e vettorializzate su vettori interi di lunghezza 500 milioni:

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

Questa parte può essere ignorata in sicurezza senza perdere continuità.

I compilatori non avranno sempre informazioni sufficienti per vettorializzare. Potremmo usare le specifiche OpenMP per la programmazione parallela , che fornisce anche una direttiva del compilatore simd per istruire i compilatori a vettorializzare il codice. È essenziale assicurarsi che non vi siano sovrapposizioni di memoria, condizioni di gara ecc. Durante la vettorializzazione manuale del codice, altrimenti si otterranno risultati errati.

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

In questo modo, chiediamo specificamente al compilatore di vettorializzarlo, non importa quale. Dovremo attivare le estensioni OpenMP usando il flag di compilazione -fopenmp. In questo modo:

# 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

che è grandioso! Questo è stato testato con gcc v6.2.0 e llvm clang v3.9.0 (entrambi installati tramite homebrew, MacOS 10.12.3), entrambi supportano OpenMP 4.0.


In questo senso, anche se la pagina Wikipedia sulla programmazione di array menziona il fatto che i linguaggi che operano su interi array di solito lo chiamano operazioni vettoriali , in realtà si nasconde l' IMO (a meno che non sia realmente vettoriale).

In caso di R, pari rowSums()o colSums()codice in C non sfruttare la vettorializzazione del codice IIUC; è solo un ciclo in C. Lo stesso vale per lapply(). In caso di apply(), è in R. Tutti questi sono quindi nascosto .

In breve, avvolgendo una funzione R:

basta scrivere un for-loop in C! = vettorializzare il tuo codice.
basta scrivere un for-loop in R! = vettorializzare il tuo codice.

Intel Math Kernel Library (MKL) ad esempio implementa forme vettoriali di funzioni.

HTH


Riferimenti:

  1. Discorso di James Reinders, Intel (questa risposta è principalmente un tentativo di riassumere questo eccellente discorso)

35

Quindi, per riassumere le grandi risposte / commenti in una risposta generale e fornire alcuni retroscena: R ha 4 tipi di loop ( dall'ordine non vettoriale a quello vettoriale )

  1. Ciclo R forche richiama ripetutamente le funzioni R in ciascuna iterazione ( non vettorializzato )
  2. C loop che chiama ripetutamente le funzioni R in ciascuna iterazione ( non vettorializzato )
  3. C loop che chiama la funzione R una sola volta ( Un po 'vettorializzato )
  4. Un semplice ciclo C che non chiama alcuna funzione R e usa le sue funzioni compilate ( Vectorized )

Quindi la *applyfamiglia è il secondo tipo. Tranne applyche è più del primo tipo

Puoi capirlo dal commento nel suo codice sorgente

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

/ * Questo è uno speciale. Interno, quindi ha argomenti non valutati. Viene
chiamato da un wrapper di chiusura, quindi X e FUN sono promesse. FUN non deve essere valutato per l'uso in es. Bquote. * /

Ciò significa che lapplyil codice C accetta una funzione non valutata da R e successivamente la valuta all'interno del codice C stesso. Questa è sostanzialmente la differenza tra lapplyla .Internalchiamata

.Internal(lapply(X, FUN))

Che ha un FUNargomento che contiene una funzione R.

E la colMeans .Internalchiamata che non ha un FUNargomento

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

colMeans, a differenza di lapplysapere esattamente quale funzione deve utilizzare, quindi calcola la media internamente all'interno del codice C.

Puoi vedere chiaramente il processo di valutazione della funzione R in ogni iterazione all'interno del lapplycodice 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);
   }

Riassumendo, lapplynon è vettorializzato , sebbene abbia due possibili vantaggi rispetto al semplice forciclo R

  1. Accedere e assegnare in un ciclo sembra essere più veloce in C (cioè lapplynell'ing di una funzione) Sebbene la differenza sembri grande, rimaniamo comunque al livello dei microsecondi e la cosa costosa è la valutazione di una funzione R in ogni iterazione. Un semplice esempio:

    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. Come accennato da @Roland, esegue un ciclo C compilato piuttosto che un ciclo R interpretato


Sebbene quando vettorizzi il tuo codice, ci sono alcune cose che devi prendere in considerazione.

  1. Se il set di dati (chiamata di let it df) è di classe data.frame, alcune funzioni vectorized (come ad esempio colMeans, colSums, rowSums, etc.) dovranno convertirlo in una matrice prima, semplicemente perché questo è il modo in cui sono stati progettati. Ciò significa che per un grande dfquesto può creare un enorme sovraccarico. Anche lapplyse non sarà necessario farlo poiché estrae i vettori effettivi da df(come data.frameè solo un elenco di vettori) e quindi, se non si hanno così tante colonne ma molte righe, a lapply(df, mean)volte può essere un'opzione migliore di colMeans(df).
  2. Un'altra cosa da ricordare è che R ha una grande varietà di diversi tipi di funzioni, come .Primitive, e generico ( S3, S4) vedere qui per alcune informazioni aggiuntive. La funzione generica deve eseguire un metodo di invio che a volte un'operazione costosa. Ad esempio, meanè una S3funzione generica mentre sumè Primitive. Quindi alcune volte lapply(df, sum)potrebbero essere molto efficienti rispetto colSumsai motivi sopra elencati

1
Riepilogo molto coerente. Solo alcune note: (1) C sa come gestire "data.frame" s, poiché sono "list" con attributi; è quello colMeansecc. che sono costruiti per gestire solo matrici. (2) Sono un po 'confuso dalla tua terza categoria; Non so dire a quale esattezza ti riferisci. (3) Dato che ti riferisci specificamente a lapply, credo che non faccia alcuna differenza tra "[<-"in R e C; entrambi pre-allocano un "elenco" (un SEXP) e lo compilano in ogni iterazione ( SET_VECTOR_ELTin C), a meno che non mi manchi il punto.
alexis_laz,

2
Capisco il fatto do.callche crea una chiamata di funzione nell'ambiente C e la valuta; anche se faccio fatica a confrontarlo con il loop o la vettorializzazione poiché fa una cosa diversa. In realtà, hai ragione sull'accesso e l'assegnazione delle differenze tra C e R, sebbene entrambi rimangano al livello dei microsecondi e non influenzino enormemente il risultato, poiché il costoso è la chiamata di funzione R iterativa (confronta R_loope R_lapplynella mia risposta ). (
Modificherò il

2
Non sto cercando di non essere d'accordo --- e sono confuso, onestamente, con ciò con cui non sei d'accordo. Il mio commento precedente avrebbe potuto essere formulato meglio. Sto cercando di perfezionare la terminologia utilizzata perché il termine "vettorializzazione" ha due definizioni che sono spesso confuse. Non penso sia discutibile. Burns e sembra che tu voglia usarlo solo nel senso dell'implementazione, ma Hadley e molti membri di R-Core (prendendo Vectorize()come esempio) lo usano anche nel senso dell'interfaccia utente. Penso che gran parte del disaccordo in questo thread sia causato dall'uso di un termine per due concetti separati ma correlati.
Gregor Thomas,

3
@DavidArenburg e non è la vettorializzazione in un senso dell'interfaccia utente, indipendentemente dal fatto che ci sia un ciclo for in R o C sotto?
Gregor Thomas,

2
@DavidArenburg, Gregor, penso che la confusione sia tra "vettorializzazione del codice" e "funzioni vettorializzate". In R, l'uso sembra propenso verso quest'ultimo. La "vettorializzazione del codice" descrive il funzionamento su un vettore di lunghezza 'k' nella stessa istruzione. Avvolgere un fn. il codice loopy si traduce in "funzioni vettorializzate" (sì, non ha senso ed è confuso, concordo, sarebbe meglio nascondere le funzioni loop / vector i / p ) e non dovrebbe avere nulla a che fare con la vettorializzazione del codice . In R, applicare sarebbe una funzione vettorializzata , ma non vettorializza il codice, ma opera su vettori.
Arun,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.