Nous avons donc l'habitude de dire à chaque nouvel utilisateur R que " apply
n'est pas vectorisé, regardez le Patrick Burns R Inferno Circle 4 " qui dit (je cite):
Un réflexe courant consiste à utiliser une fonction de la famille apply. Ce n'est pas de la vectorisation, c'est un masquage de boucle . La fonction apply a une boucle for dans sa définition. La fonction lapply enterre la boucle, mais les temps d'exécution ont tendance à être à peu près égaux à une boucle for explicite.
En effet, un rapide coup d'œil sur le apply
code source révèle la boucle:
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
Ok jusqu'à présent, mais un regard lapply
ou vapply
révèle en fait une image complètement différente:
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>
Donc, apparemment, il n'y a pas de for
boucle R cachée là-bas, ils appellent plutôt une fonction écrite interne en C.
Un rapide coup d' oeil dans le lapin trou révèle à peu près la même image
De plus, prenons la colMeans
fonction par exemple, qui n'a jamais été accusée de ne pas être vectorisée
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>
Hein? Il appelle aussi juste .Internal(colMeans(...
que nous pouvons également trouver dans le terrier du lapin . Alors, en quoi est-ce différent .Internal(lapply(..
?
En fait, un benchmark rapide révèle que les sapply
performances ne sont pas pires colMeans
et bien meilleures qu'une for
boucle pour un ensemble de données volumineuses
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
En d'autres termes, est-il correct de dire cela lapply
et vapply
sont en fait vectorisés (par rapport à apply
laquelle est une for
boucle qui appelle également lapply
) et que voulait vraiment dire Patrick Burns?
la source
*apply
les fonctions appellent à plusieurs reprises les fonctions R, ce qui les rend boucles. En ce qui concerne les bonnes performances desapply(m, mean)
: Peut-être le code C de lalapply
méthode envoie-t-il une seule fois et appelle-t-il ensuite la méthode à plusieurs reprises?mean.default
est assez optimisé.Réponses:
Tout d'abord, dans votre exemple vous faites des tests sur un "data.frame" qui n'est pas juste pour
colMeans
,apply
et"[.data.frame"
puisqu'ils ont une surcharge:Sur une matrice, l'image est un peu différente:
En reprenant la partie principale de la question, la principale différence entre
lapply
/mapply
/ etc et les boucles R simples est l'endroit où la boucle est effectuée. Comme le note Roland, les boucles C et R doivent évaluer une fonction R à chaque itération, ce qui est le plus coûteux. Les fonctions C vraiment rapides sont celles qui font tout en C, donc, je suppose, cela devrait être ce que signifie "vectorisé"?Un exemple où nous trouvons la moyenne dans chacun des éléments d'une "liste":
( EDIT 11 mai 16 : Je crois que l'exemple de recherche de la "moyenne" n'est pas une bonne configuration pour les différences entre l'évaluation d'une fonction R de manière itérative et le code compilé, (1) en raison de la particularité de l'algorithme de moyenne de R sur "numérique" s sur un simple
sum(x) / length(x)
et (2), il devrait avoir plus de sens de tester sur les «listes» aveclength(x) >> lengths(x)
. Ainsi, l'exemple «moyen» est déplacé à la fin et remplacé par un autre.)A titre d'exemple simple, nous pourrions considérer la découverte de l'opposé de chaque
length == 1
élément d'une "liste":Dans un
tmp.c
fichier:Et en face R:
avec des données:
Analyse comparative:
(Suit l'exemple original de la recherche moyenne):
la source
all_C
etC_and_R
. J'ai également trouvé dans les documentations d'compiler::cmpfun
une ancienne version R de lapply qui contient une véritablefor
boucle R , je commence à soupçonner que Burns faisait référence à cette ancienne version qui a été vectorisée depuis et c'est la vraie réponse à ma question. ..la1
de?compiler::cmpfun
semble, toujours, donner la même efficacité avec tout sauf lesall_C
fonctions. Je suppose que cela - en fait - est une question de définition; "vectorisé" signifie-t-il toute fonction qui accepte non seulement des scalaires, toute fonction qui a du code C, toute fonction qui utilise des calculs en C seulement?lapply
n'est pas vectorisé simplement parce qu'il évalue une fonction R à chaque itération avec son code C?Pour moi, la vectorisation consiste avant tout à rendre votre code plus facile à écrire et à comprendre.
Le but d'une fonction vectorisée est d'éliminer la comptabilité associée à une boucle for. Par exemple, au lieu de:
Tu peux écrire:
Cela permet de voir plus facilement ce qui est identique (les données d'entrée) et ce qui est différent (la fonction que vous appliquez).
Un avantage secondaire de la vectorisation est que la boucle for est souvent écrite en C plutôt qu'en R. Cela présente des avantages substantiels en termes de performances, mais je ne pense pas que ce soit la propriété clé de la vectorisation. La vectorisation consiste fondamentalement à sauver votre cerveau, pas à sauver le travail de l'ordinateur.
la source
for
boucles C et R. OK, une boucle C peut être optimisée par le compilateur, mais le point principal pour les performances est de savoir si le contenu de la boucle est efficace. Et évidemment, le code compilé est généralement plus rapide que le code interprété. Mais c'est probablement ce que vous vouliez dire.Je suis d'accord avec le point de vue de Patrick Burns selon lequel il s'agit plutôt de masquage de boucles et non de vectorisation de code . Voici pourquoi:
Considérez cet
C
extrait de code:Ce que nous aimerions faire est assez clair. Mais comment la tâche est effectuée ou comment elle pourrait être exécutée n'est pas vraiment. Une boucle for par défaut est une construction série. Il n'indique pas si ou comment les choses peuvent être faites en parallèle.
Le moyen le plus évident est que le code est exécuté de manière séquentielle . Chargez
a[i]
et continuezb[i]
dans les registres, ajoutez-les, stockez le résultat dansc[i]
et faites-le pour chacuni
.Cependant, les processeurs modernes ont un jeu d' instructions vectorielles ou SIMD qui est capable de fonctionner sur un vecteur de données pendant la même instruction lors de l'exécution de la même opération (par exemple, en ajoutant deux vecteurs comme indiqué ci-dessus). Selon le processeur / l'architecture, il peut être possible d'ajouter, disons, quatre nombres à partir de
a
etb
sous la même instruction, au lieu d'un à la fois.Ce serait formidable si le compilateur identifie ces blocs de code et les vectorise automatiquement , ce qui est une tâche difficile. La vectorisation automatique du code est un sujet de recherche difficile en informatique. Mais au fil du temps, les compilateurs se sont améliorés. Vous pouvez vérifier les capacités de vectorisation automatique d'
GNU-gcc
ici . De même pourLLVM-clang
ici . Et vous pouvez également trouver quelques points de repère dans le dernier lien par rapport àgcc
etICC
(compilateur Intel C ++).gcc
(I'm onv4.9
) par exemple, ne vectorise pas le code automatiquement au-O2
niveau de l'optimisation. Donc, si nous devions exécuter le code ci-dessus, il serait exécuté séquentiellement. Voici le moment pour ajouter deux vecteurs entiers d'une longueur de 500 millions.Nous devons soit ajouter le drapeau,
-ftree-vectorize
soit changer l'optimisation au niveau-O3
. (Notez que cela-O3
effectue également d' autres optimisations supplémentaires ). Le drapeau-fopt-info-vec
est utile car il informe quand une boucle a été vectorisée avec succès).Cela nous indique que la fonction est vectorisée. Voici les timings comparant les versions non vectorisées et vectorisées sur des vecteurs entiers de longueur 500 millions:
Cette partie peut être ignorée en toute sécurité sans perdre la continuité.
Les compilateurs n'auront pas toujours suffisamment d'informations pour vectoriser. Nous pourrions utiliser la spécification OpenMP pour la programmation parallèle , qui fournit également une directive de compilateur simd pour demander aux compilateurs de vectoriser le code. Il est essentiel de s'assurer qu'il n'y a pas de chevauchements de mémoire, de conditions de course, etc. lors de la vectorisation manuelle du code, sinon cela entraînera des résultats incorrects.
En faisant cela, nous demandons spécifiquement au compilateur de le vectoriser quoi qu'il arrive. Nous devrons activer les extensions OpenMP en utilisant l'indicateur de temps de compilation
-fopenmp
. En faisant cela:qui est genial! Cela a été testé avec gcc v6.2.0 et llvm clang v3.9.0 (tous deux installés via homebrew, MacOS 10.12.3), tous deux prenant en charge OpenMP 4.0.
En ce sens, même si la page Wikipedia sur Array Programming mentionne que les langages qui opèrent sur des tableaux entiers appellent généralement cela comme des opérations vectorisées , c'est vraiment une boucle qui cache l' OMI (à moins qu'elle ne soit réellement vectorisée).
Dans le cas de R, pair
rowSums()
oucolSums()
code en C n'exploitent pas la vectorisation de code IIUC; c'est juste une boucle en C. Il en va de mêmelapply()
. Dans le cas deapply()
, c'est dans R. Tous ces éléments se cachent donc en boucle .HTH
Références:
la source
Donc, pour résumer les bonnes réponses / commentaires en une réponse générale et fournir un arrière-plan: R a 4 types de boucles ( dans l'ordre non vectorisé à vectorisé )
for
Boucle R qui appelle à plusieurs reprises des fonctions R dans chaque itération ( non vectorisée )La
*apply
famille est donc le deuxième type. Saufapply
qui est plus du premier typeVous pouvez comprendre cela à partir du commentaire dans son code source
Cela signifie que le
lapply
code C accepte une fonction non évaluée de R et l'évalue plus tard dans le code C lui-même. C'est fondamentalement la différence entre l' appel delapply
s.Internal
Qui a un
FUN
argument qui contient une fonction REt l'
colMeans
.Internal
appel qui n'a pas d'FUN
argumentcolMeans
, contrairement àlapply
sait exactement quelle fonction il doit utiliser, il calcule donc la moyenne en interne dans le code C.Vous pouvez clairement voir le processus d'évaluation de la fonction R à chaque itération dans le
lapply
code CPour résumer,
lapply
n'est pas vectorisé , bien qu'il présente deux avantages possibles par rapport à lafor
boucle R simpleL'accès et l'affectation dans une boucle semblent être plus rapides en C (c'est-à-dire dans
lapply
une fonction). Bien que la différence semble grande, nous restons toujours au niveau de la microseconde et le plus coûteux est la valorisation d'une fonction R à chaque itération. Un exemple simple:Comme mentionné par @Roland, il exécute une boucle C compilée plutôt qu'une boucle R interprétée
Cependant, lors de la vectorisation de votre code, vous devez prendre en compte certains éléments.
df
) est de classedata.frame
, certaines fonctions vectorisés ( par exemplecolMeans
,colSums
,rowSums
, etc.) devront convertir en une matrice d' abord, simplement parce que c'est la façon dont ils ont été conçus. Cela signifie que pour un gros,df
cela peut créer une surcharge énorme. Bien quelapply
cela ne soit pas nécessaire car il extrait les vecteurs réels dedf
(commedata.frame
c'est juste une liste de vecteurs) et donc, si vous n'avez pas autant de colonnes mais de nombreuses lignes,lapply(df, mean)
peut parfois être une meilleure option quecolMeans(df)
..Primitive
, et générique (S3
,S4
) voir ici pour quelques informations supplémentaires. La fonction générique doit faire un envoi de méthode qui est parfois une opération coûteuse. Par exemple,mean
est uneS3
fonction générique tandis quesum
estPrimitive
. Ainsi, certains momentslapply(df, sum)
pourraient être très efficaces parcolSums
rapport aux raisons énumérées ci-dessusla source
colMeans
etc. qui sont construits pour ne gérer que des matrices. (2) Je suis un peu confus par votre troisième catégorie; Je ne peux pas dire à quoi -exaclty- vous parlez. (3) Puisque vous faites allusion spécifiquement àlapply
, je pense que cela ne fait pas de différence entre"[<-"
R et C; ils pré-allouent tous les deux une "liste" (un SEXP) et la remplissent à chaque itération (SET_VECTOR_ELT
en C), sauf si je manque votre point.do.call
en ce qu'il construit un appel de fonction dans l'environnement C et l'évalue simplement; même si j'ai du mal à le comparer à la boucle ou à la vectorisation car il fait une chose différente. Vous avez, en fait, raison d'accéder et d'attribuer des différences entre C et R, bien que les deux restent au niveau de la microseconde et n'affectent pas énormément le résultat, le résultat étant énormément coûteux, l'appel itératif de la fonction R (comparerR_loop
etR_lapply
dans ma réponse ). (Je vais éditer votre message avec un repère; j'espère que cela ne vous dérangera toujours pas)Vectorize()
comme exemple) l'utilisent également dans le sens de l'interface utilisateur. Je pense qu'une grande partie du désaccord dans ce fil est causée par l'utilisation d'un terme pour deux concepts distincts mais liés.