Więc przyzwyczailiśmy się do każdego nowego użytkownika R, że „ apply
nie jest wektoryzowany, sprawdź Patrick Burns R Inferno Circle 4 ”, który mówi (cytuję):
Powszechnym odruchem jest użycie funkcji w rodzinie stosowanej. To nie jest wektoryzacja, to ukrywanie pętli . Funkcja zastosuj w swojej definicji pętlę for. Funkcja lapply zakopuje pętlę, ale czasy wykonywania są z grubsza równe jawnej pętli for.
Rzeczywiście, szybkie spojrzenie na apply
kod źródłowy ujawnia pętlę:
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
Na razie ok, ale spojrzenie na lapply
lub vapply
faktycznie ujawnia zupełnie inny obraz:
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>
Więc najwyraźniej nie ma tam ukrytej for
pętli R , raczej wywołują wewnętrzną funkcję napisaną w C.
Szybkie spojrzenie w króliczą nory ujawnia prawie ten sam obraz
Ponadto weźmy colMeans
na przykład funkcję, której nigdy nie zarzucano, że nie jest wektoryzowana
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>
Co? To także tylko połączenia, .Internal(colMeans(...
które możemy również znaleźć w króliczej norze . Więc czym się to różni od .Internal(lapply(..
?
Właściwie szybki test porównawczy ujawnia, że sapply
działa nie gorzej niż colMeans
i znacznie lepiej niż for
pętla dla dużego zbioru danych
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
Innymi słowy, czy słuszne jest powiedzenie tego lapply
i vapply
faktycznie są one wektoryzowane (w porównaniu z apply
którym jest for
pętla, która również wywołuje lapply
) i co naprawdę miał na myśli Patrick Burns?
źródło
*apply
funkcje wielokrotnie wywołują funkcje języka R, co powoduje ich zapętlenie. Odnośnie dobrej wydajnościsapply(m, mean)
: Prawdopodobnie kod Clapply
metody wywołuje tylko raz, a następnie wywołuje ją wielokrotnie?mean.default
jest dość zoptymalizowany.Odpowiedzi:
Po pierwsze, w twoim przykładzie wykonujesz testy na „data.frame”, co nie jest sprawiedliwe
colMeans
,apply
a"[.data.frame"
ponieważ mają one narzut:Na matrycy obraz wygląda trochę inaczej:
Biorąc pod uwagę główną część pytania, główną różnicą między
lapply
/mapply
/ etc a prostymi pętlami R jest miejsce, w którym pętle są wykonywane. Jak zauważa Roland, obie pętle C i R muszą oceniać funkcję R w każdej iteracji, która jest najbardziej kosztowna. Naprawdę szybkie funkcje C to te, które robią wszystko w C, więc myślę, że o to właśnie chodzi w „wektoryzacji”?Przykład, w którym znajdujemy średnią w każdym z elementów „listy”:
( EDYTUJ 11 maja 2016 : Uważam, że przykład ze znalezieniem "średniej" nie jest dobrą konfiguracją dla różnic między iteracyjną oceną funkcji R a skompilowanym kodem, (1) ze względu na specyfikę algorytmu średniej R na "numerycznej" s ponad prostym
sum(x) / length(x)
i (2) bardziej sensowne powinno być testowanie na „liście” zlength(x) >> lengths(x)
. Tak więc przykład „średni” jest przenoszony na koniec i zastępowany innym).Jako prosty przykład możemy rozważyć znalezienie przeciwieństwa każdego
length == 1
elementu „listy”:W
tmp.c
pliku:A po stronie R:
z danymi:
Benchmarking:
(Zgodnie z oryginalnym przykładem ustalenia średniej):
źródło
all_C
iC_and_R
. I również w dokumentacjecompiler::cmpfun
w starej wersji R lapply który zawiera rzeczywistą Rfor
pętlę, zaczynam podejrzewać, że Burns odnosił się do tej starej wersji, która została wektoryzowane od tamtego czasu i jest to rzeczywista odpowiedź na moje pytanie .. ..la1
z?compiler::cmpfun
wydaje się nadal zapewniać taką samą wydajność przy wszystkichall_C
funkcjach. Myślę, że to - rzeczywiście - staje się kwestią definicji; czy „wektoryzowany” oznacza każdą funkcję, która akceptuje nie tylko skalary, jakąkolwiek funkcję, która ma kod w C, jakąkolwiek funkcję, która korzysta z obliczeń tylko w C?lapply
nie jest on wektoryzowany tylko dlatego, że ocenia funkcję R w każdej iteracji w swoim kodzie C?Dla mnie wektoryzacja to przede wszystkim ułatwienie pisania i zrozumienia kodu.
Celem funkcji wektoryzowanej jest wyeliminowanie księgowości związanej z pętlą for. Na przykład zamiast:
Możesz pisać:
Dzięki temu łatwiej jest zobaczyć, co jest takie samo (dane wejściowe), a co inne (funkcja, którą stosujesz).
Drugorzędną zaletą wektoryzacji jest to, że pętla for jest często zapisywana w języku C, a nie R. Ma to znaczące korzyści w zakresie wydajności, ale nie sądzę, że jest to kluczowa właściwość wektoryzacji. Wektoryzacja polega zasadniczo na ratowaniu mózgu, a nie pracy komputera.
źródło
for
pętlami C i R. OK, pętla C może zostać zoptymalizowana przez kompilator, ale głównym punktem wydajności jest to, czy zawartość pętli jest wydajna. Oczywiście skompilowany kod jest zwykle szybszy niż kod interpretowany. Ale prawdopodobnie to chciałeś powiedzieć.Zgadzam się z poglądem Patricka Burnsa, że jest to raczej ukrywanie w pętli, a nie wektoryzacja kodu . Dlatego:
Rozważ ten
C
fragment kodu:Co co chcielibyśmy zrobić, jest całkiem jasne. Ale to, jak zadanie jest wykonywane lub jak można je wykonać, nie jest tak naprawdę. Dla pętli domyślnie jest szeregowym konstrukt. Nie informuje, czy i jak można coś zrobić równolegle.
Najbardziej oczywistym sposobem jest to, że kod jest uruchamiany sekwencyjnie . Załaduj
a[i]
ib[i]
przejdź do rejestrów, dodaj je, zapisz wynikc[i]
i zrób to dla każdegoi
.Jednak nowoczesne procesory mają zestaw instrukcji wektorowych lub SIMD, które mogą działać na wektorze danych podczas tej samej instrukcji podczas wykonywania tej samej operacji (np. Dodawanie dwóch wektorów, jak pokazano powyżej). W zależności od procesora / architektury może być możliwe dodanie, powiedzmy, czterech liczb z
a
ib
pod tą samą instrukcją zamiast jednej naraz.Byłoby wspaniale, gdyby kompilator zidentyfikował takie bloki kodu i automatycznie je wektoryzował, co jest trudnym zadaniem. Automatyczna wektoryzacja kodu to trudny temat badawczy w informatyce. Ale z biegiem czasu kompilatory stały się w tym coraz lepsze. Możesz sprawdzić możliwości autowektoryzacji
GNU-gcc
tutaj . PodobnieLLVM-clang
tutaj . W ostatnim linku można również znaleźć testy porównawczegcc
iICC
(kompilator Intel C ++).gcc
(Jestem nav4.9
) nie wektoryzuje kodu automatycznie przy-O2
optymalizacji poziomu. Więc gdybyśmy mieli wykonać kod pokazany powyżej, byłby uruchamiany sekwencyjnie. Oto czas dodawania dwóch wektorów całkowitych o długości 500 milionów.Musimy albo dodać flagę,
-ftree-vectorize
albo zmienić optymalizację na poziom-O3
. (Zauważ, że-O3
wykonuje również inne dodatkowe optymalizacje ). Flaga-fopt-info-vec
jest przydatna, ponieważ informuje, kiedy pętla została pomyślnie wektoryzowana).To mówi nam, że funkcja jest wektoryzowana. Oto czasy porównania wersji niewektoryzowanych i wektoryzowanych na wektorach całkowitych o długości 500 milionów:
Tę część można bezpiecznie pominąć bez utraty ciągłości.
Kompilatory nie zawsze będą miały wystarczającą ilość informacji do wektoryzacji. Moglibyśmy użyć specyfikacji OpenMP do programowania równoległego , która również zapewnia rozszerzenie dyrektywę kompilatora simd, aby poinstruować kompilatory, aby wektoryzowały kod. Podczas ręcznej wektoryzacji kodu należy upewnić się, że nie ma nakładania się pamięci, warunków wyścigu itp. W przeciwnym razie spowoduje to nieprawidłowe wyniki.
Robiąc to, prosimy kompilator o wektoryzację bez względu na wszystko. Będziemy musieli aktywować rozszerzenia OpenMP za pomocą flagi czasu kompilacji
-fopenmp
. Robiąc to:który jest świetny! Zostało to przetestowane z gcc v6.2.0 i llvm clang v3.9.0 (oba zainstalowane przez homebrew, MacOS 10.12.3), z których oba obsługują OpenMP 4.0.
W tym sensie, mimo że strona Wikipedii na temat programowania tablic wspomina, że języki, które operują na całych tablicach, zwykle nazywają to operacjami wektoryzowanymi , tak naprawdę jest ukrywa pętlę IMO (chyba że jest faktycznie wektoryzowana).
W przypadku R, nawet
rowSums()
lubcolSums()
kod w C nie wykorzystuj wektoryzacji kodu IIUC; to tylko pętla w C. To samo dotyczylapply()
. W przypadkuapply()
, jest w R. Wszystkie te elementy są więc ukrywane w pętli .HTH
Bibliografia:
źródło
Podsumowując świetne odpowiedzi / komentarze w ogólną odpowiedź i dostarczając trochę informacji: R ma 4 typy pętli ( w kolejności od nie wektoryzowanej do zwektoryzowanej )
for
Pętla R, która wielokrotnie wywołuje funkcje języka R w każdej iteracji ( nie zwektoryzowany )Więc
*apply
rodzina jest drugim typem. Z wyjątkiem tego,apply
który jest bardziej pierwszego typuMożesz to zrozumieć z komentarza w jego kodzie źródłowym
Oznacza to, że
lapply
kod w języku C przyjmuje nieocenioną funkcję z języka R, a następnie ocenia ją w samym kodzie C. Jest to po prostu różnica międzylapply
ów.Internal
rozmowyKtóry ma
FUN
argument, który przechowuje funkcję R.I
colMeans
.Internal
wezwanie, które nie maFUN
argumentucolMeans
, w przeciwieństwie do tego,lapply
wie dokładnie, jakiej funkcji potrzebuje, więc oblicza średnią wewnętrznie w kodzie C.Możesz wyraźnie zobaczyć proces oceny funkcji R w każdej iteracji w
lapply
kodzie C.Podsumowując,
lapply
nie jest wektoryzowany , chociaż ma dwie możliwe zalety w stosunku do zwykłejfor
pętli R.Dostęp i przypisywanie w pętli wydaje się być szybsze w języku C (tj. W
lapply
funkcji). Chociaż różnica wydaje się duża, nadal pozostajemy na poziomie mikrosekund, a kosztowna jest wycena funkcji R w każdej iteracji. Prosty przykład:Jak wspomniał @Roland, uruchamia skompilowaną pętlę C, a raczej zinterpretowaną pętlę R.
Chociaż podczas wektoryzacji kodu należy wziąć pod uwagę kilka rzeczy.
df
) jest klasydata.frame
, niektóre vectorized funkcje (takie jakcolMeans
,colSums
,rowSums
, itd.) Będą musiały przekształcić go do macierzy po pierwsze, po prostu dlatego, że jest to w jaki sposób zostały one zaprojektowane. Oznacza to, że w przypadku dużegodf
może to spowodować ogromne obciążenie. Chociażlapply
nie będzie musiał tego robić, ponieważ wyodrębnia rzeczywiste wektory zdf
(jakdata.frame
jest to tylko lista wektorów), a zatem, jeśli masz nie tyle kolumn, ale wiele wierszy,lapply(df, mean)
czasami może być lepszą opcją niżcolMeans(df)
..Primitive
, i generic (S3
,S4
), patrz tutaj, aby uzyskać dodatkowe informacje. Funkcja ogólna musi wykonać wysyłkę metody, która czasami jest kosztowną operacją. Na przykładmean
jestS3
funkcją ogólną, podczas gdysum
jestPrimitive
. Dlatego czasamilapply(df, sum)
może być bardzo wydajne w porównaniucolSums
z przyczynami wymienionymi powyżejźródło
colMeans
itd., które są zbudowane do obsługi tylko macierzy. (2) Jestem trochę zdezorientowany twoją trzecią kategorią; Nie mogę powiedzieć, o czym mówisz - exaclty. (3) Ponieważ odnosisz się konkretnie do tegolapply
, uważam, że nie robi to różnicy między"[<-"
R i C; obaj wstępnie przydzielają „listę” (SEXP) i wypełniają ją w każdej iteracji (SET_VECTOR_ELT
w C), chyba że nie rozumiem.do.call
, że buduje wywołanie funkcji w środowisku C i po prostu ją ocenia; chociaż trudno mi porównać to z zapętleniem lub wektoryzacją, ponieważ robi to inaczej. Właściwie masz rację co do uzyskiwania dostępu i przypisywania różnic między C i R, chociaż oba pozostają na poziomie mikrosekund i nie wpływają znacząco na wynik wyniku, ponieważ kosztowne jest iteracyjne wywołanie funkcji R (porównajR_loop
iwR_lapply
mojej odpowiedzi ). (Zmienię twój post z benchmarkiem; mam nadzieję, że nadal nie będziesz miał nic przeciwko)Vectorize()
na przykładzie) używają go również w sensie interfejsu użytkownika. Myślę, że duża część nieporozumień w tym wątku jest spowodowana użyciem jednego terminu dla dwóch oddzielnych, ale powiązanych ze sobą koncepcji.