-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPerf-improve.qmd
419 lines (272 loc) · 26.5 KB
/
Perf-improve.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# Mejorando el desempeño {#sec-perf-improve}
```{r, include = FALSE}
source("common.R")
```
## Introducción
\index{performance!improving}
> Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todos los males. Sin embargo, no debemos dejar pasar nuestras oportunidades en ese crítico 3%. Un buen programador no se dejará llevar por la complacencia de tal razonamiento, será prudente al mirar cuidadosamente el código crítico; pero solo después de que ese código haya sido identificado.
>
> --- Donald Knuth
Una vez que haya utilizado la creación de perfiles para identificar un cuello de botella, debe hacerlo más rápido. Es difícil dar consejos generales sobre cómo mejorar el rendimiento, pero hago lo mejor que puedo con cuatro técnicas que se pueden aplicar en muchas situaciones. También sugeriré una estrategia general para la optimización del rendimiento que ayude a garantizar que su código más rápido siga siendo correcto.
Es fácil quedar atrapado tratando de eliminar todos los cuellos de botella. ¡No! Su tiempo es valioso y es mejor gastarlo analizando sus datos, no eliminando posibles ineficiencias en su código. Sea pragmático: no gaste horas de su tiempo para ahorrar segundos de tiempo de computadora. Para hacer cumplir este consejo, debe establecer un objetivo de tiempo para su código y optimizar solo hasta ese objetivo. Esto significa que no eliminará todos los cuellos de botella. Algunas no las alcanzarás porque has cumplido tu objetivo. Es posible que deba pasar por alto otros y aceptarlos porque no hay una solución rápida y fácil o porque el código ya está bien optimizado y no es posible una mejora significativa. Acepte estas posibilidades y pase al siguiente candidato.
Si desea obtener más información sobre las características de rendimiento del lenguaje R, le recomiendo *Evaluar el diseño del lenguaje R* [@r-design]. Saca conclusiones al combinar un intérprete R modificado con un amplio conjunto de código que se encuentra en la naturaleza.
### Estructura {.unnumbered}
- La @sec-code-organisation le enseña cómo organizar su código para que la optimización sea lo más fácil y libre de errores posible.
- La @sec-already-solved le recuerda que busque las soluciones existentes.
- La @sec-be-lazy enfatiza la importancia de ser perezoso: a menudo, la forma más fácil de hacer una función más rápida es dejar que haga menos trabajo.
- La @sec-vectorise define de forma concisa la vectorización y le muestra cómo aprovechar al máximo las funciones integradas.
- La @sec-avoid-copies analiza los peligros de rendimiento de la copia de datos.
- La @sec-t-test reúne todas las piezas en un estudio de caso que muestra cómo acelerar las pruebas t repetidas unas mil veces.
- La @sec-more-techniques termina el capítulo con indicaciones a más recursos que lo ayudarán a escribir código rápido.
### Requisitos previos {.unnumbered}
Usaremos [bench](https://bench.r-lib.org/) para comparar con precisión el rendimiento de pequeños fragmentos de código independientes.
```{r setup}
library(bench)
```
## Organización del código {#sec-code-organisation}
\index{performance!strategy}
Hay dos trampas en las que es fácil caer cuando intentas hacer tu código más rápido:
1. Escribir código más rápido pero incorrecto.
2. Escribir código que crees que es más rápido, pero que en realidad no es mejor.
La estrategia descrita a continuación le ayudará a evitar estas trampas.
Al abordar un cuello de botella, es probable que encuentre múltiples enfoques. Escriba una función para cada enfoque, encapsulando todo el comportamiento relevante. Esto hace que sea más fácil verificar que cada enfoque devuelva el resultado correcto y cronometrar cuánto tiempo lleva ejecutarse. Para demostrar la estrategia, compararé dos enfoques para calcular la media:
```{r}
mean1 <- function(x) mean(x)
mean2 <- function(x) sum(x) / length(x)
```
Te recomiendo que lleves un registro de todo lo que intentes, incluso de los fracasos. Si ocurre un problema similar en el futuro, será útil ver todo lo que ha intentado. Para hacer esto, recomiendo RMarkdown, que facilita la combinación de código con comentarios y notas detallados.
A continuación, genere un caso de prueba representativo. El caso debe ser lo suficientemente grande para capturar la esencia de su problema, pero lo suficientemente pequeño como para que solo tome unos segundos como máximo. No desea que tarde demasiado porque necesitará ejecutar el caso de prueba muchas veces para comparar enfoques. Por otro lado, no desea que el caso sea demasiado pequeño porque es posible que los resultados no alcancen el problema real. Aquí voy a usar 100,000 números:
```{r}
x <- runif(1e5)
```
Ahora usa `bench::mark()` para comparar con precisión las variaciones. `bench::mark()` verifica automáticamente que todas las llamadas devuelvan los mismos valores. Esto no garantiza que la función se comporte de la misma manera para todas las entradas, por lo que en un mundo ideal también tendrá pruebas unitarias para asegurarse de no cambiar accidentalmente el comportamiento de la función.
```{r}
bench::mark(
mean1(x),
mean2(x)
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
(Puede que te sorprendan los resultados: `mean(x)` es considerablemente más lento que `sum(x) / length(x)`. Esto se debe a que, entre otras razones, `mean(x)` hace dos pasadas sobre el vector para que sea numéricamente más preciso.)
Si desea ver esta estrategia en acción, la he usado varias veces en stackoverflow:
- <http://stackoverflow.com/questions/22515525#22518603>
- <http://stackoverflow.com/questions/22515175#22515856>
- <http://stackoverflow.com/questions/3476015#22511936>
## Comprobación de soluciones existentes {#sec-already-solved}
Una vez que haya organizado su código y capturado todas las variaciones que se le ocurran, es natural ver lo que otros han hecho. Eres parte de una gran comunidad y es muy posible que alguien ya haya abordado el mismo problema. Dos buenos lugares para comenzar son:
- [CRAN task views](http://cran.rstudio.com/web/views/). Si hay una vista de tareas CRAN relacionada con el dominio de su problema, vale la pena mirar los paquetes enumerados allí.
- Dependencias inversas de Rcpp, como se indica en su [página de CRAN](http://cran.r-project.org/web/packages/Rcpp). Dado que estos paquetes usan C++, es probable que sean rápidos.
De lo contrario, el desafío es describir su cuello de botella de una manera que lo ayude a encontrar problemas y soluciones relacionados. Saber el nombre del problema o sus sinónimos hará que esta búsqueda sea mucho más fácil. Pero como no sabes cómo se llama, ¡es difícil buscarlo! La mejor manera de resolver este problema es leer mucho para que puedas construir tu propio vocabulario con el tiempo. Alternativamente, pregunte a otros. Hable con sus colegas y haga una lluvia de ideas sobre algunos nombres posibles, luego busque en Google y StackOverflow. Suele ser útil restringir la búsqueda a páginas relacionadas con R. Para Google, pruebe [rseek](http://www.rseek.org/). Para stackoverflow, restrinja su búsqueda incluyendo la etiqueta R, `[R]`, en su búsqueda.
Registre todas las soluciones que encuentre, no solo aquellas que parezcan ser más rápidas inmediatamente. Algunas soluciones pueden ser más lentas inicialmente, pero terminan siendo más rápidas porque son más fáciles de optimizar. También puede combinar las partes más rápidas desde diferentes enfoques. Si ha encontrado una solución lo suficientemente rápida, ¡felicidades! De lo contrario, sigue leyendo.
### Ejercicios
1. ¿Cuáles son las alternativas más rápidas a `lm()`? ¿Cuáles están diseñados específicamente para trabajar con conjuntos de datos más grandes?
2. ¿Qué paquete implementa una versión de `match()` que es más rápida para búsquedas repetidas? ¿Cuánto más rápido es?
3. Enumere cuatro funciones (no solo las de base R) que convierten una cadena en un objeto de fecha y hora. Cuales son sus fortalezas y debilidades?
4. ¿Qué paquetes brindan la capacidad de calcular una media móvil?
5. ¿Cuáles son las alternativas a `optim()`?
## Haciendo lo menos posible {#sec-be-lazy}
La forma más fácil de hacer que una función sea más rápida es dejar que haga menos trabajo. Una forma de hacerlo es usar una función adaptada a un tipo de entrada o salida más específico, o a un problema más específico. Por ejemplo:
- `rowSums()`, `colSums()`, `rowMeans()`, y `colMeans()` son más rápidas que las invocaciones equivalentes que usan `apply()` porque están vectorizadas (@sec-vectorise).
- `vapply()` es más rápido que `sapply()` porque especifica previamente el tipo de salida.
- Si quiere ver si un vector contiene un solo valor, `any(x == 10)` es mucho más rápido que `10 %in% x` porque probar la igualdad es más simple que probar la inclusión de conjuntos.
Tener este conocimiento al alcance de la mano requiere saber que existen funciones alternativas: es necesario tener un buen vocabulario. Amplíe su vocabulario leyendo regularmente el código R. Buenos lugares para leer código son la [lista de correo de R-help](https://stat.ethz.ch/mailman/listinfo/r-help) y [StackOverflow](http://stackoverflow.com/questions/tagged/%20r).
Algunas funciones obligan a sus entradas a un tipo específico. Si su entrada no es del tipo correcto, la función tiene que hacer un trabajo extra. En su lugar, busque una función que funcione con sus datos tal como están, o considere cambiar la forma en que almacena sus datos. El ejemplo más común de este problema es usar `apply()` en un marco de datos. `apply()` siempre convierte su entrada en una matriz. No solo es propenso a errores (porque un marco de datos es más general que una matriz), sino que también es más lento.
Otras funciones harán menos trabajo si les proporciona más información sobre el problema. Siempre vale la pena leer detenidamente la documentación y experimentar con diferentes argumentos. Algunos ejemplos que he descubierto en el pasado incluyen:
- `read.csv()`: especificar tipos de columnas conocidas con `colClasses`. (También considere cambiar a `readr::read_csv()` o `data.table::fread()` que son considerablemente más rápidos que `read.csv()`.)
- `factor()`: especificar niveles conocidos con `levels`.
- `cut()`: no genere etiquetas con `labels = FALSE` si no las necesita o, mejor aún, use `findInterval()` como se menciona en la sección "ver también" de la documentación.
- `unlist(x, use.names = FALSE)` es mucho más rápido que `unlist(x)`.
- `interaction()`: si solo necesita combinaciones que existen en los datos, use `drop = TRUE`.
A continuación, exploro cómo podría mejorar la aplicación de esta estrategia para mejorar el rendimiento de `mean()` y `as.data.frame()`.
### `mean()`
\index{.Internal()}
\index{method dispatch!performance}
A veces, puede hacer que una función sea más rápida evitando el envío de métodos. Si está llamando a un método en un ciclo cerrado, puede evitar algunos de los costos haciendo la búsqueda del método solo una vez:
- Para S3, puede hacer esto llamando a `generic.class()` en lugar de `generic()`.
- Para S4, puede hacer esto usando `selectMethod()` para encontrar el método, guardándolo en una variable y luego llamando a esa función.
Por ejemplo, llamar a `mean.default()` es un poco más rápido que llamar a `mean()` para vectores pequeños:
```{r}
x <- runif(1e2)
bench::mark(
mean(x),
mean.default(x)
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
Esta optimización es un poco arriesgada. Si bien `mean.default()` es casi el doble de rápido para 100 valores, fallará de manera sorprendente si `x` no es un vector numérico.
Una optimización aún más arriesgada es llamar directamente a la función `.Internal` subyacente. Esto es más rápido porque no realiza ninguna verificación de entrada ni maneja NA, por lo que está comprando velocidad a costa de la seguridad.
```{r}
x <- runif(1e2)
bench::mark(
mean(x),
mean.default(x),
.Internal(mean(x))
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
NB: La mayoría de estas diferencias surgen porque `x` es pequeño. Si aumenta el tamaño, las diferencias básicamente desaparecen, porque la mayor parte del tiempo ahora se dedica a calcular la media, sin encontrar la implementación subyacente. Este es un buen recordatorio de que el tamaño de la entrada es importante y debe motivar sus optimizaciones en función de datos realistas.
```{r}
x <- runif(1e4)
bench::mark(
mean(x),
mean.default(x),
.Internal(mean(x))
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
### `as.data.frame()`
\index{as.data.frame()}
Saber que está tratando con un tipo específico de entrada puede ser otra forma de escribir código más rápido. Por ejemplo, `as.data.frame()` es bastante lento porque convierte cada elemento en un marco de datos y luego `rbind()` los une. Si tiene una lista con nombre con vectores de igual longitud, puede transformarla directamente en un marco de datos. En este caso, si puede hacer suposiciones sólidas sobre su entrada, puede escribir un método que sea considerablemente más rápido que el predeterminado.
```{r}
quickdf <- function(l) {
class(l) <- "data.frame"
attr(l, "row.names") <- .set_row_names(length(l[[1]]))
l
}
l <- lapply(1:26, function(i) runif(1e3))
names(l) <- letters
bench::mark(
as.data.frame = as.data.frame(l),
quick_df = quickdf(l)
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
Una vez más, tenga en cuenta la compensación. Este método es rápido porque es peligroso. Si le da entradas incorrectas, obtendrá un marco de datos corrupto:
```{r}
quickdf(list(x = 1, y = 1:2))
```
Para llegar a este método mínimo, leí cuidadosamente y luego reescribí el código fuente para `as.data.frame.list()` y `data.frame()`. Hice muchos pequeños cambios, comprobando cada vez que no había roto el comportamiento existente. Después de varias horas de trabajo, pude aislar el código mínimo que se muestra arriba. Esta es una técnica muy útil. La mayoría de las funciones básicas de R están escritas para la flexibilidad y la funcionalidad, no para el rendimiento. Por lo tanto, reescribir para su necesidad específica a menudo puede generar mejoras sustanciales. Para hacer esto, deberá leer el código fuente. Puede ser complejo y confuso, ¡pero no te rindas!
### Ejercicios
1. ¿Cuál es la diferencia entre `rowSums()` y `.rowSums()`?
2. Cree una versión más rápida de `chisq.test()` que solo calcula la estadística de prueba de chi-cuadrado cuando la entrada son dos vectores numéricos sin valores faltantes. Puede intentar simplificar `chisq.test()` o codificar desde la [definición matemática](http://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test).
3. ¿Puedes hacer una versión más rápida de `table()` para el caso de una entrada de dos vectores enteros sin valores perdidos? ¿Puedes usarlo para acelerar tu prueba de chi-cuadrado?
## Vectorizar {#sec-vectorise}
\index{vectorisation}
Si ha usado R durante algún tiempo, probablemente haya escuchado la advertencia de "vectorizar su código". Pero, ¿qué significa eso realmente? Vectorizar su código no se trata solo de evitar bucles for, aunque eso suele ser un paso. Vectorizar se trata de adoptar un enfoque de objeto completo para un problema, pensando en vectores, no en escalares. Hay dos atributos clave de una función vectorizada:
- Simplifica muchos problemas. En lugar de tener que pensar en los componentes de un vector, solo piensa en vectores completos.
- Los bucles en una función vectorizada están escritos en C en lugar de R. Los bucles en C son mucho más rápidos porque tienen mucha menos sobrecarga.
El @sec-functionals hizo hincapié en la importancia del código vectorizado como una abstracción de mayor nivel. La vectorización también es importante para escribir código R rápido. Esto no significa simplemente usar `map()` o `lapply()`. En cambio, la vectorización significa encontrar la función R existente que se implementa en C y se aplica más a su problema.
Las funciones vectorizadas que se aplican a muchos cuellos de botella de rendimiento comunes incluyen:
- `rowSums()`, `colSums()`, `rowMeans()`, y `colMeans()`. Estas funciones matriciales vectorizadas siempre serán más rápidas que usar `apply()`. A veces puede usar estas funciones para construir otras funciones vectorizadas.
```{r}
rowAny <- function(x) rowSums(x) > 0
rowAll <- function(x) rowSums(x) == ncol(x)
```
- La creación de subconjuntos vectorizados puede conducir a grandes mejoras en la velocidad. Recuerde las técnicas detrás de las tablas de búsqueda (@sec-lookup-tables) y la combinación y combinación manual (@sec-matching-merging). Recuerde también que puede usar la asignación de subconjuntos para reemplazar varios valores en un solo paso. Si `x` es un vector, una matriz o un marco de datos, entonces `x[is.na(x)] <- 0` reemplazará todos los valores faltantes con 0.
- Si está extrayendo o reemplazando valores en ubicaciones dispersas en una matriz o marco de datos, subconjunto con una matriz de enteros. Consulte @sec-matrix-subsetting para obtener más detalles.
- Si está convirtiendo valores continuos a categóricos, asegúrese de saber cómo usar `cut()` y `findInterval()`.
- Tenga en cuenta las funciones vectorizadas como `cumsum()` y `diff()`.
El álgebra matricial es un ejemplo general de vectorización. Estos bucles son ejecutados por bibliotecas externas altamente optimizadas como BLAS. Si puede encontrar una manera de usar el álgebra matricial para resolver su problema, a menudo obtendrá una solución muy rápida. La habilidad para resolver problemas con álgebra matricial es producto de la experiencia. Un buen lugar para comenzar es preguntar a personas con experiencia en su dominio.
La vectorización tiene un inconveniente: es más difícil predecir cómo escalarán las operaciones. El siguiente ejemplo mide cuánto tiempo lleva usar subconjuntos de caracteres para buscar 1, 10 y 100 elementos de una lista. Podría esperar que buscar 10 elementos tomara 10 veces más que buscar 1, y que buscar 100 elementos tomaría 10 veces más de nuevo. De hecho, el siguiente ejemplo muestra que solo se tarda aproximadamente \~10 veces más en buscar 100 elementos que en buscar 1. Eso sucede porque una vez que llega a un cierto tamaño, la implementación interna cambia a una estrategia que tiene un mayor costo de instalación, pero escala mejor.
```{r}
lookup <- setNames(as.list(sample(100, 26)), letters)
x1 <- "j"
x10 <- sample(letters, 10)
x100 <- sample(letters, 100, replace = TRUE)
bench::mark(
lookup[x1],
lookup[x10],
lookup[x100],
check = FALSE
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
La vectorización no resolverá todos los problemas y, en lugar de convertir un algoritmo existente en uno que utilice un enfoque vectorizado, a menudo es mejor escribir su propia función vectorizada en C++. Aprenderá cómo hacerlo en el @sec-rcpp.
### Ejercicios
1. Las funciones de densidad, por ejemplo, `dnorm()`, tienen una interfaz común. ¿Qué argumentos se vectorizan? ¿Qué hace `rnorm(10, mean = 10:1)`?
2. Compara la velocidad de `apply(x, 1, sum)` con `rowSums(x)` para diferentes tamaños de `x`.
3. ¿Cómo puedes usar `crossprod()` para calcular una suma ponderada? ¿Cuánto más rápido es que el ingenuo `sum(x * w)`?
## Evitar copias {#sec-avoid-copies}
\index{loops!avoiding copies in} \index{paste()}
Una fuente perniciosa de código R lento es hacer crecer un objeto con un bucle. Siempre que use `c()`, `append()`, `cbind()`, `rbind()` o `paste()` para crear un objeto más grande, R primero debe asignar espacio para el nuevo objeto y luego copiar el objeto antiguo a su nuevo hogar. Si repite esto muchas veces, como en un ciclo for, esto puede ser bastante costoso. Has entrado en el Círculo 2 del [*R inferno*](http://www.burns-stat.com/pages/Tutor/R_inferno.pdf).
Viste un ejemplo de este tipo de problema en la @sec-memory-profiling, así que aquí mostraré un ejemplo un poco más complejo del mismo problema básico. Primero generamos algunas cadenas aleatorias y luego las combinamos iterativamente con un ciclo usando `collapse()`, o en un solo paso usando `paste()`. Tenga en cuenta que el rendimiento de `collapse()` empeora relativamente a medida que aumenta el número de cadenas: combinar 100 cadenas lleva casi 30 veces más que combinar 10 cadenas.
```{r}
random_string <- function() {
paste(sample(letters, 50, replace = TRUE), collapse = "")
}
strings10 <- replicate(10, random_string())
strings100 <- replicate(100, random_string())
collapse <- function(xs) {
out <- ""
for (x in xs) {
out <- paste0(out, x)
}
out
}
bench::mark(
loop10 = collapse(strings10),
loop100 = collapse(strings100),
vec10 = paste(strings10, collapse = ""),
vec100 = paste(strings100, collapse = ""),
check = FALSE
)[c("expression", "min", "median", "itr/sec", "n_gc")]
```
Modificar un objeto en un bucle, por ejemplo, `x[i] <- y`, también puede crear una copia, dependiendo de la clase de `x`. La @sec-single-binding analiza este problema con mayor profundidad y le brinda algunas herramientas para determinar cuándo está haciendo copias.
## Caso de estudio: t-test {#sec-t-test}
El siguiente estudio de caso muestra cómo hacer que las pruebas t sean más rápidas utilizando algunas de las técnicas descritas anteriormente. Se basa en un ejemplo de [*Cálculo de miles de estadísticas de prueba simultáneamente en R*](http://stat-computing.org/newsletter/issues/scgn-18-1.pdf) de Holger Schwender y Tina Müller. Recomiendo encarecidamente leer el documento completo para ver la misma idea aplicada a otras pruebas.
Imagine que hemos realizado 1000 experimentos (filas), cada uno de los cuales recopila datos de 50 individuos (columnas). Los primeros 25 individuos de cada experimento se asignan al grupo 1 y el resto al grupo 2. Primero generaremos algunos datos aleatorios para representar este problema:
```{r}
m <- 1000
n <- 50
X <- matrix(rnorm(m * n, mean = 10, sd = 3), nrow = m)
grp <- rep(1:2, each = n / 2)
```
Para los datos en este formulario, hay dos formas de usar `t.test()`. Podemos usar la interfaz de fórmula o proporcionar dos vectores, uno para cada grupo. El tiempo revela que la interfaz de la fórmula es considerablemente más lenta.
```{r, cache = TRUE}
system.time(
for (i in 1:m) {
t.test(X[i, ] ~ grp)$statistic
}
)
system.time(
for (i in 1:m) {
t.test(X[i, grp == 1], X[i, grp == 2])$statistic
}
)
```
Por supuesto, un bucle for calcula, pero no guarda los valores. Podemos `map_dbl()` (@sec-map-atomic) para hacer eso. Esto agrega un poco de sobrecarga:
```{r}
compT <- function(i){
t.test(X[i, grp == 1], X[i, grp == 2])$statistic
}
system.time(t1 <- purrr::map_dbl(1:m, compT))
```
¿Cómo podemos hacer esto más rápido? Primero, podríamos intentar hacer menos trabajo. Si observa el código fuente de `stats:::t.test.default()`, verá que hace mucho más que calcular la estadística t. También calcula el valor p y formatea la salida para su impresión. Podemos intentar que nuestro código sea más rápido eliminando esas piezas.
```{r}
my_t <- function(x, grp) {
t_stat <- function(x) {
m <- mean(x)
n <- length(x)
var <- sum((x - m) ^ 2) / (n - 1)
list(m = m, n = n, var = var)
}
g1 <- t_stat(x[grp == 1])
g2 <- t_stat(x[grp == 2])
se_total <- sqrt(g1$var / g1$n + g2$var / g2$n)
(g1$m - g2$m) / se_total
}
system.time(t2 <- purrr::map_dbl(1:m, ~ my_t(X[.,], grp)))
stopifnot(all.equal(t1, t2))
```
Esto nos da una mejora de velocidad de seis veces.
Ahora que tenemos una función bastante simple, podemos hacerla aún más rápida al vectorizarla. En lugar de recorrer la matriz fuera de la función, modificaremos `t_stat()` para que funcione con una matriz de valores. Por lo tanto, `mean()` se convierte en `rowMeans()`, `length()` se convierte en `ncol()` y `sum()` se convierte en `rowSums()`. El resto del código permanece igual.
```{r}
rowtstat <- function(X, grp){
t_stat <- function(X) {
m <- rowMeans(X)
n <- ncol(X)
var <- rowSums((X - m) ^ 2) / (n - 1)
list(m = m, n = n, var = var)
}
g1 <- t_stat(X[, grp == 1])
g2 <- t_stat(X[, grp == 2])
se_total <- sqrt(g1$var / g1$n + g2$var / g2$n)
(g1$m - g2$m) / se_total
}
system.time(t3 <- rowtstat(X, grp))
stopifnot(all.equal(t1, t3))
```
¡Eso es mucho más rápido! Es al menos 40 veces más rápido que nuestro esfuerzo anterior y alrededor de 1000 veces más rápido que donde comenzamos.
<!-- Estas comparaciones de tiempo no se reflejan en el código. En la copia en pdf esta última función tarda 0,011 s mientras que la versión original tarda 0,191 s (unas 17 veces más lento). ¿Quizás hubo una mejora en la versión base de t.test? -->
## Otras tecnicas {#sec-more-techniques}
Ser capaz de escribir código R rápido es parte de ser un buen programador R. Más allá de las sugerencias específicas de este capítulo, si desea escribir código R rápido, deberá mejorar sus habilidades generales de programación. Algunas formas de hacer esto son:
- [Read R blogs](http://www.r-bloggers.com/) para ver con qué problemas de rendimiento han luchado otras personas y cómo han hecho que su código sea más rápido.
- Lea otros libros de programación R, como *El arte de la programación R* [@art-r-prog] o \[*R Inferno*\] de Patrick Burns (http://www.burns-stat.com/documents/books/the -r-inferno/) para conocer las trampas comunes.
- Tome un curso de algoritmos y estructura de datos para aprender algunas formas bien conocidas de abordar ciertas clases de problemas. Escuché cosas buenas sobre el [curso de algoritmos de Princeton](https://www.coursera.org/course/algs4partI) que se ofrece en Coursera.
- Aprende a paralelizar tu código. Dos lugares para comenzar son *Parallel R* [@parallel-r] y *Parallel Computing for Data Science* [@parcomp-ds].
- Lea libros generales sobre optimización como *Optimización madura* [@mature-opt] o el *Programador pragmático* [@pragprog].
También puede comunicarse con la comunidad para obtener ayuda. StackOverflow puede ser un recurso útil. Deberá esforzarse un poco para crear un ejemplo fácilmente digerible que también capture las características más destacadas de su problema. Si su ejemplo es demasiado complejo, pocas personas tendrán el tiempo y la motivación para intentar una solución. Si es demasiado simple, obtendrá respuestas que resuelven el problema del juguete pero no el problema real. Si también intenta responder preguntas en StackOverflow, rápidamente tendrá una idea de lo que constituye una buena pregunta.