Así que estamos acostumbrados a decirle a cada nuevo usuario de R que " apply
no está vectorizado, vea el Círculo Infernal 4 de Patrick Burns R " que dice (cito):
Un reflejo común es usar una función en la familia de postulantes. Esto no es vectorización, está ocultando bucles . La función de aplicación tiene un bucle for en su definición. La función lapply entierra el ciclo, pero los tiempos de ejecución tienden a ser aproximadamente iguales a un ciclo for explícito.
De hecho, un vistazo rápido al apply
código fuente revela el ciclo:
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
Ok hasta ahora, pero un vistazo lapply
o vapply
revela una imagen completamente diferente:
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>
Entonces, aparentemente no hay un for
bucle R escondido allí, sino que llaman a una función escrita en C interna.
Una mirada rápida en el conejo agujero revela más o menos la misma imagen
Además, tomemos la colMeans
función, por ejemplo, que nunca fue acusada de no ser vectorizada.
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? También solo llama .Internal(colMeans(...
que también podemos encontrar en la madriguera del conejo . Entonces, ¿cómo es esto diferente de .Internal(lapply(..
?
En realidad, un punto de referencia rápido revela que sapply
no funciona peor colMeans
y mucho mejor que un for
bucle para un gran conjunto de datos
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 otras palabras, ¿es correcto decir eso lapply
y en vapply
realidad están vectorizados (en comparación con lo apply
que es un for
ciclo que también llama lapply
) y qué quería decir realmente Patrick Burns?
fuente
*apply
las funciones llaman repetidamente a las funciones R, lo que las convierte en bucles. Con respecto al buen rendimiento desapply(m, mean)
: ¿Posiblemente el código C delapply
método se despacha solo una vez y luego llama al método repetidamente?mean.default
Está bastante optimizado.Respuestas:
En primer lugar, en su ejemplo que realizar pruebas en un "hoja.de.datos" que no es justo para
colMeans
,apply
y"[.data.frame"
ya que tienen una sobrecarga:En una matriz, la imagen es un poco diferente:
Regulando la parte principal de la pregunta, la principal diferencia entre
lapply
/mapply
/ etc. y los R-loops directos es dónde se realiza el bucle. Como señala Roland, los bucles C y R deben evaluar una función R en cada iteración, que es la más costosa. Las funciones C realmente rápidas son aquellas que hacen todo en C, así que, supongo, ¿esto debería ser de lo que se trata la "vectorización"?Un ejemplo donde encontramos la media en cada uno de los elementos de una "lista":
( EDITAR 11 de mayo de 16 : creo que el ejemplo de encontrar la "media" no es una buena configuración para las diferencias entre evaluar una función R de forma iterativa y un código compilado, (1) debido a la particularidad del algoritmo medio de R en "numérico" s sobre un simple
sum(x) / length(x)
y (2) debería tener más sentido probar en "listas" conlength(x) >> lengths(x)
. Entonces, el ejemplo "medio" se mueve al final y se reemplaza por otro.)Como un ejemplo simple, podríamos considerar el hallazgo del opuesto de cada
length == 1
elemento de una "lista":En un
tmp.c
archivo:Y en el lado R:
con datos:
Benchmarking:
(Sigue el ejemplo original de hallazgo medio):
fuente
all_C
yC_and_R
funciones. También encontré en las documentaciones decompiler::cmpfun
una versión R antigua de lapply que contiene unfor
bucle R real , estoy empezando a sospechar que Burns se refería a esa versión antigua que se vectorizó desde entonces y esta es la respuesta real a mi pregunta ... ..la1
de?compiler::cmpfun
parece, todavía, producir la misma eficiencia con todas lasall_C
funciones menos. Supongo que, de hecho, se trata de una cuestión de definición; ¿está "vectorizado", es decir, cualquier función que acepte no solo escalares, cualquier función que tenga código C, alguna función que use cálculos en C solamente?lapply
no está vectorizado simplemente porque está evaluando una función R en cada iteración dentro de su código C?Para mí, la vectorización se trata principalmente de hacer que su código sea más fácil de escribir y más fácil de entender.
El objetivo de una función vectorizada es eliminar la contabilidad asociada con un bucle for. Por ejemplo, en lugar de:
Puedes escribir:
Eso hace que sea más fácil ver qué es lo mismo (los datos de entrada) y qué es diferente (la función que está aplicando).
Una ventaja secundaria de la vectorización es que el bucle for a menudo se escribe en C, en lugar de en R. Esto tiene beneficios de rendimiento sustanciales, pero no creo que sea la propiedad clave de la vectorización. La vectorización se trata fundamentalmente de salvar su cerebro, no de salvar el trabajo de la computadora.
fuente
for
bucles C y R. OK, el compilador puede optimizar un bucle C, pero el punto principal para el rendimiento es si el contenido del bucle es eficiente. Y, obviamente, el código compilado suele ser más rápido que el código interpretado. Pero eso es probablemente lo que querías decir.Estoy de acuerdo con el punto de vista de Patrick Burns de que se trata de ocultar bucles y no de vectorización de código . Este es el por qué:
Considere este
C
fragmento de código:Lo que nos gustaría hacer es bastante claro. Pero cómo se realiza la tarea o cómo se podría realizar no es realmente. Un bucle for por defecto es una construcción en serie. No informa si las cosas se pueden hacer en paralelo o cómo.
La forma más obvia es que el código se ejecuta de manera secuencial . Cargue
a[i]
y continúeb[i]
en los registros, agréguelos, almacene el resultadoc[i]
y haga esto para cada unoi
.Sin embargo, los procesadores modernos tienen un conjunto de instrucciones vectoriales o SIMD que es capaz de operar en un vector de datos durante la misma instrucción al realizar la misma operación (por ejemplo, agregando dos vectores como se muestra arriba). Dependiendo del procesador / arquitectura, podría ser posible agregar, digamos, cuatro números de
a
yb
bajo la misma instrucción, en lugar de uno a la vez.Sería genial si el compilador identifica dichos bloques de código y los vectoriza automáticamente , lo cual es una tarea difícil. La vectorización automática de código es un tema de investigación desafiante en informática. Pero con el tiempo, los compiladores han mejorado en eso. Puede verificar las capacidades de vectorización automática
GNU-gcc
aquí . Del mismo modo porLLVM-clang
aquí . Y también puede encontrar algunos puntos de referencia en el último enlace comparado congcc
yICC
(compilador Intel C ++).gcc
(Estoy encendidov4.9
), por ejemplo, no vectoriza el código automáticamente en la-O2
optimización de nivel. Entonces, si tuviéramos que ejecutar el código que se muestra arriba, se ejecutaría secuencialmente. Aquí está el momento para agregar dos vectores enteros de 500 millones de longitud.Necesitamos agregar la bandera
-ftree-vectorize
o cambiar la optimización al nivel-O3
. (Tenga en cuenta que también-O3
realiza otras optimizaciones adicionales ). La bandera-fopt-info-vec
es útil ya que informa cuando un ciclo se vectorizó con éxito).Esto nos dice que la función está vectorizada. Estos son los tiempos que comparan las versiones no vectorizadas y vectorizadas en vectores enteros de 500 millones de longitud:
Esta parte se puede omitir de forma segura sin perder continuidad.
Los compiladores no siempre tendrán información suficiente para vectorizar. Podríamos usar la especificación OpenMP para programación paralela , que también proporciona una directiva de compilación simd para instruir a los compiladores a vectorizar el código. Es esencial asegurarse de que no haya superposiciones de memoria, condiciones de carrera, etc. al vectorizar el código manualmente, de lo contrario, se obtendrán resultados incorrectos.
Al hacer esto, le pedimos específicamente al compilador que lo vectorice sin importar qué. Tendremos que activar las extensiones de OpenMP utilizando el indicador de tiempo de compilación
-fopenmp
. Haciendo eso:¡Lo cual es genial! Esto se probó con gcc v6.2.0 y llvm clang v3.9.0 (ambos instalados a través de homebrew, MacOS 10.12.3), los cuales son compatibles con OpenMP 4.0.
En este sentido, a pesar de que la página de Wikipedia sobre la programación de matrices menciona que los lenguajes que operan en matrices enteras generalmente lo llaman operaciones vectorizadas , en realidad se trata de ocultar IMO (a menos que en realidad esté vectorizado).
En el caso de R, incluso
rowSums()
o elcolSums()
código en C no explota la vectorización de código IIUC; es solo un bucle en C. Lo mismo vale paralapply()
. En caso deapply()
que esté en R. Todos estos son, por lo tanto, de bucle oculto .HTH
Referencias
fuente
Entonces, para resumir las excelentes respuestas / comentarios en una respuesta general y proporcionar algunos antecedentes: R tiene 4 tipos de bucles ( desde el orden no vectorizado hasta el vectorizado )
for
loop que llama repetidamente funciones R en cada iteración ( no vectorizado )Entonces la
*apply
familia es el segundo tipo. Exceptoapply
cuál es más del primer tipoPuedes entender esto por el comentario en su código fuente
Eso significa que
lapply
el código C acepta una función no evaluada de R y luego la evalúa dentro del propio código C. Esta es básicamente la diferencia entrelapply
la.Internal
llamada sQue tiene un
FUN
argumento que tiene una función RY la
colMeans
.Internal
llamada que no tieneFUN
argumentocolMeans
, a diferencia delapply
sabe exactamente qué función necesita usar, por lo tanto, calcula la media internamente dentro del código C.Puede ver claramente el proceso de evaluación de la función R en cada iteración dentro del
lapply
código CEn resumen,
lapply
no está vectorizado , aunque tiene dos posibles ventajas sobre elfor
bucle R simple.El acceso y la asignación en un bucle parece ser más rápido en C (es decir, en
lapply
una función) Aunque la diferencia parece grande, todavía nos quedamos en el nivel de microsegundos y lo costoso es la valoración de una función R en cada iteración. Un simple ejemplo:Como mencionó @Roland, ejecuta un bucle C compilado en lugar de un bucle R interpretado
Aunque al vectorizar su código, hay algunas cosas que debe tener en cuenta.
df
) es de clasedata.frame
, algunas funciones vectorizada (como por ejemplocolMeans
,colSums
,rowSums
, etc.) tendrán que convertirla en una primera matriz, simplemente porque esta es la forma en que fueron diseñados. Esto significa que para un grandedf
esto puede crear una gran sobrecarga. Si bienlapply
no tendrá que hacer esto, ya que extrae los vectores reales dedf
(comodata.frame
es solo una lista de vectores) y, por lo tanto, si no tiene tantas columnas sino muchas filas, alapply(df, mean)
veces puede ser una mejor opción quecolMeans(df)
..Primitive
, y genéricos (S3
,S4
) vea aquí para obtener información adicional. La función genérica tiene que hacer un envío de método que a veces es una operación costosa. Por ejemplo,mean
es unaS3
función genérica mientrassum
esPrimitive
. Por lo tanto, algunas veceslapply(df, sum)
podría ser muy eficiente en comparacióncolSums
con las razones enumeradas anteriormentefuente
colMeans
etc., que están diseñados para manejar solo matrices. (2) Estoy un poco confundido por su tercera categoría; No puedo decir a qué -exaclty- te refieres. (3) Ya que te refieres específicamentelapply
, creo que no hace una diferencia entre"[<-"
R y C; ambos preasignan una "lista" (un SEXP) y la completan en cada iteración (SET_VECTOR_ELT
en C), a menos que me esté perdiendo su punto.do.call
de que construye una llamada de función en el entorno C y simplemente lo evalúa; aunque estoy teniendo dificultades para compararlo con el bucle o la vectorización, ya que hace algo diferente. En realidad, tiene razón sobre el acceso y la asignación de diferencias entre C y R, aunque ambos permanecen en el nivel de microsegundos y no afectan el resultado en gran medida, ya que lo costoso es la llamada de función R iterativa (compararR_loop
yR_lapply
en mi respuesta ) (Editaré tu publicación con un punto de referencia; espero que, aún así, no teVectorize()
como ejemplo) también lo usan en el sentido de la interfaz de usuario. Creo que gran parte del desacuerdo en este hilo es causado por el uso de un término para dos conceptos separados pero relacionados.