Ist die "* apply" -Familie wirklich nicht vektorisiert?

138

Wir sind es also gewohnt, jedem neuen R-Benutzer zu sagen, dass " applynicht vektorisiert ist, sehen Sie sich den Patrick Burns R Inferno Circle 4 an ", in dem steht (ich zitiere):

Ein häufiger Reflex ist die Verwendung einer Funktion in der Apply-Familie. Dies ist keine Vektorisierung, sondern ein Schleifenverstecken . Die Apply-Funktion hat eine for-Schleife in ihrer Definition. Die Lapply-Funktion vergräbt die Schleife, aber die Ausführungszeiten entsprechen in der Regel ungefähr einer expliziten for-Schleife.

In der Tat zeigt ein kurzer Blick auf den applyQuellcode die Schleife:

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

Ok, aber ein Blick auf lapplyoder vapplyzeigt tatsächlich ein ganz anderes Bild:

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>

Anscheinend forversteckt sich dort keine R- Schleife, sondern sie rufen die interne C-Schreibfunktion auf.

Ein kurzer Blick in das Kaninchenloch zeigt so ziemlich das gleiche Bild

Nehmen wir colMeanszum Beispiel die Funktion, die niemals beschuldigt wurde, nicht vektorisiert worden zu sein

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>

Huh? Es ruft auch nur an, .Internal(colMeans(...was wir auch im Kaninchenbau finden können . Wie unterscheidet sich das von .Internal(lapply(..?

Tatsächlich zeigt ein schneller Benchmark, dass eine Schleife für einen großen Datensatz sapplynicht schlechter colMeansund viel besser als eine forSchleife ist

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 

Mit anderen Worten, ist es richtig, das zu sagen lapplyund vapply tatsächlich vektorisiert zu sein (im Vergleich zu applyeiner forSchleife, die auch aufruft lapply), und was wollte Patrick Burns wirklich sagen?

David Arenburg
quelle
8
Dies ist alles in der Semantik, aber ich würde sie nicht als vektorisiert betrachten. Ich betrachte einen vektorisierten Ansatz, wenn eine R-Funktion nur einmal aufgerufen wird und ein Wertevektor übergeben werden kann. *applyFunktionen rufen wiederholt R-Funktionen auf, wodurch sie Schleifen bilden. In Bezug auf die gute Leistung von sapply(m, mean): Möglicherweise wird der C-Code von lapplynur einmal versendet und die Methode dann wiederholt aufgerufen? mean.defaultist ziemlich optimiert.
Roland
4
Ausgezeichnete Frage und vielen Dank, dass Sie den zugrunde liegenden Code überprüft haben. Ich habe gesucht, ob es kürzlich geändert wurde, aber nichts darüber in R-Versionshinweisen ab Version 2.13.0.
Ilir
1
Inwieweit hängt die Leistung sowohl von der Plattform als auch von den verwendeten C-Compiler- und Linker-Flags ab?
smci
3
@ DavidArenburg Eigentlich glaube ich nicht, dass es gut definiert ist. Zumindest kenne ich keinen kanonischen Bezug. Die Sprachdefinition erwähnt "vektorisierte" Operationen, definiert jedoch keine Vektorisierung.
Roland
3
Sehr verwandt: Ist Rs Anwendungsfamilie mehr als syntaktischer Zucker? (Und wie diese Antworten auch eine gute Lektüre.)
Gregor Thomas

Antworten:

73

Zunächst führen Sie in Ihrem Beispiel Tests an einem "data.frame" durch, für den dies nicht fair ist colMeans, applyund "[.data.frame"da diese einen Overhead haben:

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

Auf einer Matrix ist das Bild etwas anders:

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

Wenn man den Hauptteil der Frage erneut betrachtet, besteht der Hauptunterschied zwischen lapply/ mapply/ etc und einfachen R-Schleifen darin, wo die Schleife ausgeführt wird. Wie Roland bemerkt, müssen sowohl C- als auch R-Schleifen in jeder Iteration eine R-Funktion bewerten, was am teuersten ist. Die wirklich schnellen C-Funktionen sind diejenigen, die alles in C tun. Ich denke, das sollte es sein, worum es bei "vektorisiert" geht?

Ein Beispiel, bei dem wir den Mittelwert in jedem Element einer "Liste" finden:

( EDIT 11. Mai 16 : Ich glaube, das Beispiel mit dem Finden des "Mittelwerts" ist kein guter Aufbau für die Unterschiede zwischen der iterativen Bewertung einer R-Funktion und kompiliertem Code, (1) aufgrund der Besonderheit des Mittelwertalgorithmus von R für "numerisch" s über eine einfache sum(x) / length(x)und (2) es sollte sinnvoller sein, auf "Liste" s mit zu testen length(x) >> lengths(x). Also wird das "mittlere" Beispiel an das Ende verschoben und durch ein anderes ersetzt.)

Als einfaches Beispiel könnten wir das Finden des Gegenteils jedes length == 1Elements einer "Liste" betrachten:

In einer tmp.cDatei:

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

Und auf der R-Seite:

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

mit Daten:

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

Benchmarking:

#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

(Folgt dem ursprünglichen Beispiel für die Mittelwertfindung):

#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
quelle
10
Ein großartiger Punkt zu den Kosten für die Konvertierung des data.frame in eine Matrix und vielen Dank für die Bereitstellung von Benchmarks.
Joshua Ulrich
Das ist eine sehr schöne Antwort, obwohl ich Ihre all_Cund C_and_RFunktionen nicht kompilieren konnte . Ich habe auch in den Dokumentationen compiler::cmpfuneiner alten R-Version von lapply gefunden, die eine tatsächliche R- forSchleife enthält. Ich beginne zu vermuten, dass Burns sich auf diese alte Version bezog, die seitdem vektorisiert wurde, und dies ist die eigentliche Antwort auf meine Frage. ..
David Arenburg
@DavidArenburg: Benchmarking la1von ?compiler::cmpfunscheint immer noch die gleiche Effizienz mit allen außer all_CFunktionen zu erzielen . Ich denke, es ist eine Frage der Definition; bedeutet "vektorisiert" eine Funktion, die nicht nur Skalare akzeptiert, eine Funktion mit C-Code, eine Funktion, die nur Berechnungen in C verwendet?
alexis_laz
1
Ich denke, alle Funktionen in R enthalten C-Code, einfach weil alles in R eine Funktion ist (die in einer Sprache geschrieben werden musste). Wenn ich es richtig verstehe, sagen Sie also, dass dies lapplynicht einfach vektorisiert ist, weil es eine R-Funktion in jeder Iteration mit ihrem C-Code auswertet?
David Arenburg
5
@ DavidArenburg: Wenn ich "Vektorisierung" auf irgendeine Weise definieren muss, würde ich wohl einen sprachlichen Ansatz wählen; dh eine Funktion, die akzeptiert und weiß, wie man mit einem "Vektor" umgeht, ob er schnell, langsam, in C, in R oder irgendetwas anderem geschrieben ist. In R besteht die Bedeutung der Vektorisierung darin, dass viele Funktionen in C geschrieben sind und Vektoren verarbeiten, während Benutzer in anderen Sprachen normalerweise die Eingabe durchlaufen, um den Mittelwert zu ermitteln. Dadurch bezieht sich die Vektorisierung indirekt auf Geschwindigkeit, Effizienz, Sicherheit und Robustheit.
alexis_laz
65

Bei der Vektorisierung geht es mir in erster Linie darum, Ihren Code leichter zu schreiben und verständlicher zu machen.

Das Ziel einer vektorisierten Funktion besteht darin, die mit einer for-Schleife verbundene Buchhaltung zu beseitigen. Zum Beispiel anstelle von:

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

Du kannst schreiben:

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

Das macht es einfacher zu sehen, was gleich ist (die Eingabedaten) und was anders ist (die Funktion, die Sie anwenden).

Ein sekundärer Vorteil der Vektorisierung besteht darin, dass die for-Schleife häufig in C und nicht in R geschrieben wird. Dies hat erhebliche Leistungsvorteile, aber ich denke nicht, dass dies die Schlüsseleigenschaft der Vektorisierung ist. Bei der Vektorisierung geht es im Wesentlichen darum, Ihr Gehirn zu retten, nicht die Computerarbeit.

Hadley
quelle
5
Ich glaube nicht, dass es einen bedeutenden Leistungsunterschied zwischen C- und R- forSchleifen gibt. OK, eine C-Schleife kann vom Compiler optimiert werden, aber der Hauptpunkt für die Leistung ist, ob der Inhalt der Schleife effizient ist. Und offensichtlich ist kompilierter Code normalerweise schneller als interpretierter Code. Aber das wollten Sie wahrscheinlich sagen.
Roland
3
@ Roland ja, es ist nicht die for-Schleife selbst an sich, es ist alles um sie herum (die Kosten eines Funktionsaufrufs, die Möglichkeit, Änderungen an Ort und Stelle vorzunehmen, ...).
Hadley
10
@ DavidArenburg "Unnötige Konsistenz ist der Hobgoblin der kleinen Köpfe";)
Hadley
6
Nein, ich denke nicht, dass Leistung der Hauptgrund für die Vektorisierung Ihres Codes ist. Das Umschreiben einer Schleife in eine Runde ist vorteilhaft, auch wenn sie nicht schneller ist. Der Hauptpunkt von dplyr ist, dass es einfacher ist, Datenmanipulationen auszudrücken (und es ist einfach sehr schön, dass es auch schnell ist).
Hadley
12
@ DavidArenburg, weil Sie ein erfahrener R-Benutzer sind. Die meisten neuen Benutzer finden Loops viel natürlicher und müssen zum Vektorisieren ermutigt werden. Für mich geht es bei der Verwendung einer Funktion wie colMeans nicht unbedingt um Vektorisierung, sondern um die Wiederverwendung von schnellem Code, den bereits jemand geschrieben hat
Hadley
49

Ich stimme der Ansicht von Patrick Burns zu, dass es sich eher um ein Schleifenverstecken und nicht um eine Codevektorisierung handelt . Hier ist der Grund:

Betrachten Sie dieses CCode-Snippet:

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

Was wir tun möchten, ist ganz klar. Aber wie die Aufgabe ausgeführt wird oder wie sie ausgeführt werden könnte, ist nicht wirklich. Eine for-Schleife ist standardmäßig ein serielles Konstrukt. Es gibt keine Auskunft darüber, ob oder wie Dinge parallel erledigt werden können.

Der naheliegendste Weg ist, dass der Code sequentiell ausgeführt wird . Laden Sie a[i]und b[i]weiter in Register, fügen Sie sie hinzu, speichern Sie das Ergebnis in c[i]und tun Sie dies für jedes Register i.

Moderne Prozessoren verfügen jedoch über einen Vektor- oder SIMD- Befehlssatz, der in der Lage ist, einen Datenvektor während desselben Befehls zu bearbeiten, wenn dieselbe Operation ausgeführt wird (z. B. Hinzufügen von zwei Vektoren wie oben gezeigt). Abhängig vom Prozessor / der Architektur kann es möglich sein, beispielsweise vier Zahlen von aund bunter derselben Anweisung anstelle von jeweils einer Nummer hinzuzufügen .

Wir möchten die Single Instruction Multiple Data ausnutzen und Parallelität auf Datenebene durchführen , dh 4 Dinge gleichzeitig laden, 4 Dinge gleichzeitig hinzufügen, 4 Dinge gleichzeitig speichern, zum Beispiel. Und das ist Codevektorisierung .

Beachten Sie, dass sich dies von der Code-Parallelisierung unterscheidet, bei der mehrere Berechnungen gleichzeitig ausgeführt werden.

Es wäre großartig, wenn der Compiler solche Codeblöcke identifizieren und automatisch vektorisieren würde, was eine schwierige Aufgabe ist. Die automatische Codevektorisierung ist ein herausforderndes Forschungsthema in der Informatik. Aber im Laufe der Zeit sind Compiler besser geworden. Sie können die Kontroll Auto Vektorisierung Fähigkeiten GNU-gcc hier . Ähnliches gilt für LLVM-clang hier . Außerdem finden Sie im letzten Link einige Benchmarks im Vergleich zu gccund ICC(Intel C ++ - Compiler).

gcc(Ich bin eingeschaltet v4.9) zum Beispiel vektorisiert Code bei der Ebenenoptimierung nicht automatisch -O2. Wenn wir also den oben gezeigten Code ausführen würden, würde er nacheinander ausgeführt. Hier ist der Zeitpunkt für das Hinzufügen von zwei ganzzahligen Vektoren mit einer Länge von 500 Millionen.

Wir müssen entweder das Flag hinzufügen -ftree-vectorizeoder die Optimierung auf Level ändern -O3. (Beachten Sie, dass -O3auch andere zusätzliche Optimierungen durchgeführt werden.) Das Flag -fopt-info-vecist nützlich, da es darüber informiert, wann eine Schleife erfolgreich vektorisiert wurde.

# 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    

Dies sagt uns, dass die Funktion vektorisiert ist. Hier sind die Zeitabläufe, in denen sowohl nicht vektorisierte als auch vektorisierte Versionen auf ganzzahligen Vektoren mit einer Länge von 500 Millionen verglichen werden:

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

Dieser Teil kann sicher übersprungen werden, ohne die Kontinuität zu verlieren.

Compiler verfügen nicht immer über ausreichende Informationen zum Vektorisieren. Wir könnten die OpenMP-Spezifikation für die parallele Programmierung verwenden , die auch eine simd- Compiler-Direktive bereitstellt , um Compiler anzuweisen, den Code zu vektorisieren. Es ist wichtig sicherzustellen, dass es keine Speicherüberlappungen, Rennbedingungen usw. gibt, wenn Code manuell vektorisiert wird, da dies sonst zu falschen Ergebnissen führt.

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

Auf diese Weise bitten wir den Compiler ausdrücklich, es zu vektorisieren, egal was passiert. Wir müssen OpenMP-Erweiterungen mithilfe des Kompilierungszeit-Flags aktivieren -fopenmp. Auf diese Weise:

# 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

was toll ist! Dies wurde mit gcc v6.2.0 und llvm clang v3.9.0 (beide über Homebrew, MacOS 10.12.3 installiert) getestet, die beide OpenMP 4.0 unterstützen.


In diesem Sinne, obwohl die Wikipedia-Seite zur Array-Programmierung erwähnt, dass Sprachen, die mit ganzen Arrays arbeiten, dies normalerweise als vektorisierte Operationen bezeichnen , ist es tatsächlich eine Schleife, die IMO versteckt (es sei denn, sie ist tatsächlich vektorisiert).

Im Fall von R nutzen gerade rowSums()oder colSums()Code in C die Codevektorisierung IIUC nicht aus. es ist nur eine Schleife in C. Gleiches gilt für lapply(). Im Falle von apply()ist es in R. Alle diese sind daher Schleifen versteckt .

Kurz gesagt: Umschließen einer R-Funktion durch:

Schreiben Sie einfach eine for-Schleife in C! = Vektorisieren Sie Ihren Code.
Schreiben Sie einfach eine for-Schleife in R! = Vektorisieren Sie Ihren Code.

Die Intel Math Kernel Library (MKL) implementiert beispielsweise vektorisierte Funktionsformen.

HTH


Verweise:

  1. Vortrag von James Reinders, Intel (diese Antwort ist hauptsächlich ein Versuch, diesen ausgezeichneten Vortrag zusammenzufassen)
Arun
quelle
35

Um die großartigen Antworten / Kommentare zu einer allgemeinen Antwort zusammenzufassen und Hintergrundinformationen zu liefern: R hat 4 Arten von Schleifen ( von nicht vektorisierter zu vektorisierter Reihenfolge )

  1. R- forSchleife, die in jeder Iteration wiederholt R-Funktionen aufruft ( nicht vektorisiert )
  2. C-Schleife, die in jeder Iteration wiederholt R-Funktionen aufruft ( nicht vektorisiert )
  3. C-Schleife, die die R-Funktion nur einmal aufruft ( etwas vektorisiert )
  4. Eine einfache C-Schleife, die überhaupt keine R-Funktion aufruft und eigene kompilierte Funktionen verwendet ( vektorisiert )

Die *applyFamilie ist also der zweite Typ. Außer applywas eher vom ersten Typ ist

Sie können dies anhand des Kommentars im Quellcode verstehen

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

/ * Dies ist eine spezielle .Internal, hat also nicht bewertete Argumente. Es wird
von einer Verschlussverpackung aufgerufen, daher sind X und FUN Versprechen. FUN muss für die Verwendung in z. B. bquote nicht bewertet werden. * /

lapplyDies bedeutet, dass der C-Code eine nicht bewertete Funktion von R akzeptiert und sie später im C-Code selbst auswertet. Dies ist im Grunde der Unterschied zwischen lapplys .InternalAnruf

.Internal(lapply(X, FUN))

Welches hat ein FUNArgument, das eine R-Funktion enthält

Und der colMeans .InternalRuf, der nicht ein FUNArgument

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

colMeansIm Gegensatz zu lapplyweiß genau, welche Funktion es verwenden muss, berechnet es daher den Mittelwert intern innerhalb des C-Codes.

Sie können den Bewertungsprozess der R-Funktion in jeder Iteration innerhalb des lapplyC-Codes deutlich sehen

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

Zusammenfassend lässt lapplysich sagen , dass es nicht vektorisiert ist , obwohl es zwei mögliche Vorteile gegenüber der einfachen R- forSchleife hat

  1. Der Zugriff auf und die Zuweisung in einer Schleife scheint in C schneller zu sein (dh in lapplyeiner Funktion). Obwohl der Unterschied groß erscheint, bleiben wir dennoch auf der Mikrosekundenebene und das Kostspielige ist die Bewertung einer R-Funktion in jeder Iteration. Ein einfaches Beispiel:

    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. Wie von @Roland erwähnt, wird eine kompilierte C-Schleife und keine interpretierte R-Schleife ausgeführt


Bei der Vektorisierung Ihres Codes müssen jedoch einige Dinge berücksichtigt werden.

  1. Wenn Ihr Datensatz (nennen wir es df) der Klasse ist data.frame, einige vektorisiert Funktionen (wie colMeans, colSums, rowSumsusw.) müssen sie zuerst in eine Matrix konvertieren, einfach weil das ist , wie sie entworfen wurden. Dies bedeutet, dass dies für einen großen Unternehmen dfeinen enormen Overhead verursachen kann. Während lapplywird dies nicht zu tun , da es die tatsächlichen Vektoren aus Extrakten df(wie data.framenur eine Liste von Vektoren ist) und damit, wenn Sie nicht so viele Spalten , aber viele Zeilen haben, lapply(df, mean)kann manchmal als bessere Option sein colMeans(df).
  2. Eine andere Sache, an die Sie sich erinnern sollten, ist, dass R eine große Vielfalt verschiedener Funktionstypen hat, wie z. B. .Primitiveund generic ( S3, S4). Hier finden Sie einige zusätzliche Informationen. Die generische Funktion muss einen Methodenversand durchführen, der manchmal eine kostspielige Operation ist. Zum Beispiel meanist generische S3Funktion, während sumist Primitive. Daher lapply(df, sum)könnten einige Zeiten colSumsaus den oben genannten Gründen sehr effizient sein
David Arenburg
quelle
1
Sehr zusammenhängende Zusammenfassung. Nur ein paar Anmerkungen: (1) C weiß, wie man mit "data.frame" umgeht, da es sich um "list" mit Attributen handelt; Es ist das colMeansusw., das nur für Matrizen ausgelegt ist. (2) Ich bin etwas verwirrt von Ihrer dritten Kategorie; Ich kann nicht sagen, worauf Sie sich genau beziehen. (3) Da Sie sich speziell darauf beziehen lapply, glaube ich, dass es keinen Unterschied zwischen "[<-"R und C macht; Beide weisen eine "Liste" (ein SEXP) vorab zu und füllen sie in jeder Iteration ( SET_VECTOR_ELTin C) aus, es sei denn, ich vermisse Ihren Punkt.
alexis_laz
2
Ich do.callverstehe, dass es einen Funktionsaufruf in der C-Umgebung erstellt und ihn nur auswertet. obwohl es mir schwer fällt, es mit Looping oder Vektorisierung zu vergleichen, da es etwas anderes macht. Sie haben tatsächlich Recht, wenn Sie auf Unterschiede zwischen C und R zugreifen und diese zuweisen, obwohl beide auf der Mikrosekundenebene bleiben und das Ergebnis nicht wesentlich beeinflussen, da der iterative R-Funktionsaufruf teuer ist (vergleiche R_loopund R_lapplyin meiner Antwort) ). (Ich werde Ihren Beitrag mit einem Benchmark bearbeiten; ich hoffe, es macht Ihnen trotzdem nichts aus)
alexis_laz
2
Ich versuche nicht zu widersprechen - und ich bin ehrlich gesagt verwirrt darüber, womit Sie nicht einverstanden sind. Mein früherer Kommentar hätte besser formuliert werden können. Ich versuche, die verwendete Terminologie zu verfeinern, da der Begriff "Vektorisierung" zwei Definitionen hat, die häufig miteinander verschmelzen. Ich denke nicht, dass dies fraglich ist. Burns und Sie scheinen es nur im Sinne der Implementierung verwenden zu wollen, aber Hadley und viele R-Core-Mitglieder (am Vectorize()Beispiel) verwenden es auch im Sinne der Benutzeroberfläche. Ich denke, dass ein Großteil der Meinungsverschiedenheiten in diesem Thread durch die Verwendung eines Begriffs für zwei getrennte, aber verwandte Konzepte verursacht wird.
Gregor Thomas
3
@DavidArenburg und ist das nicht Vektorisierung im Sinne der Benutzeroberfläche, unabhängig davon, ob es eine for-Schleife in R oder C darunter gibt?
Gregor Thomas
2
@ DavidArenburg, Gregor, ich denke, die Verwechslung besteht zwischen "Code-Vektorisierung" und "vektorisierten Funktionen". In R scheint die Verwendung zu letzterem geneigt zu sein. "Codevektorisierung" beschreibt das Arbeiten an einem Vektor der Länge 'k' in derselben Anweisung. Ein Fn einwickeln. Um Schleifencode herum entstehen "vektorisierte Funktionen" (ja, es macht keinen Sinn und ist verwirrend, ich stimme zu, besser wäre Schleifenverstecken oder Vektor-I / P-Funktionen ) und muss nichts mit Codevektorisierung zu tun haben . In R wäre apply eine vektorisierte Funktion , die Ihren Code jedoch nicht vektorisiert, sondern mit Vektoren arbeitet.
Arun