Wir sind es also gewohnt, jedem neuen R-Benutzer zu sagen, dass " apply
nicht 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 apply
Quellcode 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 lapply
oder vapply
zeigt 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 for
versteckt 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 colMeans
zum 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 sapply
nicht schlechter colMeans
und viel besser als eine for
Schleife 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 lapply
und vapply
tatsächlich vektorisiert zu sein (im Vergleich zu apply
einer for
Schleife, die auch aufruft lapply
), und was wollte Patrick Burns wirklich sagen?
quelle
*apply
Funktionen rufen wiederholt R-Funktionen auf, wodurch sie Schleifen bilden. In Bezug auf die gute Leistung vonsapply(m, mean)
: Möglicherweise wird der C-Code vonlapply
nur einmal versendet und die Methode dann wiederholt aufgerufen?mean.default
ist ziemlich optimiert.Antworten:
Zunächst führen Sie in Ihrem Beispiel Tests an einem "data.frame" durch, für den dies nicht fair ist
colMeans
,apply
und"[.data.frame"
da diese einen Overhead haben:Auf einer Matrix ist das Bild etwas anders:
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 testenlength(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 == 1
Elements einer "Liste" betrachten:In einer
tmp.c
Datei:Und auf der R-Seite:
mit Daten:
Benchmarking:
(Folgt dem ursprünglichen Beispiel für die Mittelwertfindung):
quelle
all_C
undC_and_R
Funktionen nicht kompilieren konnte . Ich habe auch in den Dokumentationencompiler::cmpfun
einer alten R-Version von lapply gefunden, die eine tatsächliche R-for
Schleife 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. ..la1
von?compiler::cmpfun
scheint immer noch die gleiche Effizienz mit allen außerall_C
Funktionen 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?lapply
nicht einfach vektorisiert ist, weil es eine R-Funktion in jeder Iteration mit ihrem C-Code auswertet?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:
Du kannst schreiben:
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.
quelle
for
Schleifen 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.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
C
Code-Snippet: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]
undb[i]
weiter in Register, fügen Sie sie hinzu, speichern Sie das Ergebnis inc[i]
und tun Sie dies für jedes Registeri
.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
a
undb
unter derselben Anweisung anstelle von jeweils einer Nummer hinzuzufügen .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ürLLVM-clang
hier . Außerdem finden Sie im letzten Link einige Benchmarks im Vergleich zugcc
undICC
(Intel C ++ - Compiler).gcc
(Ich bin eingeschaltetv4.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-vectorize
oder die Optimierung auf Level ändern-O3
. (Beachten Sie, dass-O3
auch andere zusätzliche Optimierungen durchgeführt werden.) Das Flag-fopt-info-vec
ist nützlich, da es darüber informiert, wann eine Schleife erfolgreich vektorisiert wurde.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:
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.
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: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()
odercolSums()
Code in C die Codevektorisierung IIUC nicht aus. es ist nur eine Schleife in C. Gleiches gilt fürlapply()
. Im Falle vonapply()
ist es in R. Alle diese sind daher Schleifen versteckt .HTH
Verweise:
quelle
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 )
for
Schleife, die in jeder Iteration wiederholt R-Funktionen aufruft ( nicht vektorisiert )Die
*apply
Familie ist also der zweite Typ. Außerapply
was eher vom ersten Typ istSie können dies anhand des Kommentars im Quellcode verstehen
lapply
Dies 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 zwischenlapply
s.Internal
AnrufWelches hat ein
FUN
Argument, das eine R-Funktion enthältUnd der
colMeans
.Internal
Ruf, der nicht einFUN
ArgumentcolMeans
Im Gegensatz zulapply
weiß 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
lapply
C-Codes deutlich sehenZusammenfassend lässt
lapply
sich sagen , dass es nicht vektorisiert ist , obwohl es zwei mögliche Vorteile gegenüber der einfachen R-for
Schleife hatDer Zugriff auf und die Zuweisung in einer Schleife scheint in C schneller zu sein (dh in
lapply
einer 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: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.
df
) der Klasse istdata.frame
, einige vektorisiert Funktionen (wiecolMeans
,colSums
,rowSums
usw.) 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 Unternehmendf
einen enormen Overhead verursachen kann. Währendlapply
wird dies nicht zu tun , da es die tatsächlichen Vektoren aus Extraktendf
(wiedata.frame
nur 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 seincolMeans(df)
..Primitive
und 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 Beispielmean
ist generischeS3
Funktion, währendsum
istPrimitive
. Daherlapply(df, sum)
könnten einige ZeitencolSums
aus den oben genannten Gründen sehr effizient seinquelle
colMeans
usw., 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 beziehenlapply
, 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_ELT
in C) aus, es sei denn, ich vermisse Ihren Punkt.do.call
verstehe, 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 (vergleicheR_loop
undR_lapply
in meiner Antwort) ). (Ich werde Ihren Beitrag mit einem Benchmark bearbeiten; ich hoffe, es macht Ihnen trotzdem nichts aus)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.