-
Notifications
You must be signed in to change notification settings - Fork 19
/
20-debuggage-performance.qmd
513 lines (360 loc) · 24.9 KB
/
20-debuggage-performance.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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Débuggage et performance {#sec-debug-perf}
{{< include _setup.qmd >}}
## Débugguer une fonction
Lorsqu'on commence à écrire des fonctions un peu complexes, avec des `if`, des `for`, et autres joyeusetés, arrive forcément un moment où ça ne fonctionne pas comme on le souhaiterait et où on ne comprend pas pourquoi. Bref, *il y a un bug*.
Trouver la cause d'un bug n'est pas toujours évident, mais il existe plusieurs méthodes et outils permettant de faciliter un peu les choses.
On commence par charger les extensions et les jeux de données dont on aura besoin par la suite.
```{r message=FALSE}
library(tidyverse)
library(questionr)
data(hdv2003)
data(rp2018)
data(starwars)
```
### print()
L'outil le plus simple, rudimentaire et parfois décrié mais qui reste efficace, est de rajouter un `print()` dans le code de notre fonction pour examiner le contenu d'un objet à un moment donné.
Prenons un exemple. La fonction suivante prend en argument un tableau de données `df` et un nom de variable `var`, et retourne la moyenne et l'écart-type de cette variable.
```{r}
indicateurs <- function(df, var) {
valeurs <- df[, var]
if (!is.numeric(valeurs)) return(NA)
list(
moyenne = mean(valeurs, na.rm = TRUE),
sd = sd(valeurs, na.rm = TRUE)
)
}
```
On teste notre fonction sur une variable du jeu de données `hdv2003`. Tout semble fonctionner.
```{r}
indicateurs(hdv2003, "age")
```
On teste à nouveau, cette fois sur une variable du jeu de données `starwars` de `dplyr`.
```{r}
indicateurs(starwars, "height")
```
Sapristi, on obtient `NA` en résultat alors que notre variable `height` est bien numérique : on devrait donc obtenir la moyenne et l'écart-type. Comment expliquer ce résultat ?
Comme on n'a pas d'explication immédiate juste en relisant le code de notre fonction, on va ajouter temporairement une instruction `print()` qui va afficher le contenu de `valeurs` juste après qu'elle soit calculée.
```{r}
indicateurs <- function(df, var) {
valeurs <- df[, var]
print(valeurs)
if (!is.numeric(valeurs)) return(NA)
list(
moyenne = mean(valeurs, na.rm = TRUE),
sd = sd(valeurs, na.rm = TRUE)
)
}
```
On lance cette fonction modifiée sur `starwars` :
```{r}
indicateurs(starwars, "height")
```
Le `print()` nous indique que `valeurs` n'est pas un vecteur mais un tableau de données à une seule colonne. Or dans ce cas, le test `is.numeric`, qui est sensé s'appliquer à un vecteur atomique, renvoie `FALSE`.
```{r}
is.numeric(starwars[, "height"])
```
C'est donc la raison pour laquelle on obtient `NA` comme résultat.
Certes, mais alors pourquoi cela fonctionne-t-il dans notre exemple avec `hdv2003` ?
```{r}
indicateurs(hdv2003, "age")
```
Ah ! Dans ce cas là `valeurs` est bien un vecteur, et on obtient donc le résultat attendu. Pourquoi cette différence ? On est en fait tombé sur une différence de comportement entre les *data frames* et les *tibbles*, mentionnée @sec-df-sel : lorsqu'on sélectionne une seule colonne avec l'opérateur `[,]`, un *data frame* retourne un vecteur tandis qu'un *tibble* retourne un tableau à une colonne. Or, ici, `hdv2003` est un *data frame*, et `starwars` un *tibble*.
Comment résoudre ce problème ? Il y a différentes manières, mais la plus simple est sans doute de remplacer `[,]` par `[[]]`, qui lui a le même comportement dans les deux cas. On peut donc modifier notre fonction (en n'oubliant pas d'enlever le `print()`) et vérifier que ça fonctionne désormais.
```{r}
indicateurs <- function(df, var) {
valeurs <- df[[var]]
if (!is.numeric(valeurs)) return(NA)
list(
moyenne = mean(valeurs, na.rm = TRUE),
sd = sd(valeurs, na.rm = TRUE)
)
}
indicateurs(hdv2003, "age")
indicateurs(starwars, "height")
```
::: {.callout-warning}
Quand on ajoute des `print()` pour essayer d'identifier un problème, il faut bien penser à les supprimer une fois ce problème résolu, sinon on risque de se retrouver avec des messages "parasites" dans la console.
:::
### Localiser une erreur
`print()` peut aussi être utile pour savoir à quel moment une erreur se produit, notamment lorsqu'on utilise une boucle.
Dans l'exemple suivant, on crée une fonction `min_freq_values()` qui prend en argument un tableau de données `df` et un effectif `n_min` et retourne une liste des modalités de variables qui apparaissent dans au moins `n_min` lignes de `df` (on utilise ici une boucle `for`, mais on aurait aussi pu utiliser `map()`).
```{r}
min_freq_values <- function(df, n_min) {
res <- list()
for (col in names(df)) {
# Tri à plat des valeurs de la colonne
freq <- table(df[[col]])
# On conserve les modalités avec effectif >= n
freq <- freq[freq >= n_min]
# On ajoute la colonne au résultat s'il y a au moins une modalité
if (length(freq) > 0) res[[col]] <- freq
}
res
}
```
Si on applique `min_freq_values()` à `hdv2003` avec une valeur de `n` à 1500, on obtient toutes les modalités correspondant à au moins 1500 observations.
```{r}
min_freq_values(hdv2003, 1500)
```
Essayons maintenant d'appliquer `min_freq_values()` au jeu de données `starwars` de `dplyr`.
```{r error=TRUE}
min_freq_values(starwars, 50)
```
On obtient un message quelque peu sybillin : apparemment une erreur se produit au moment de faire le tri à plat des valeurs avec `table()`. Mais comme ce tri à plat s'effectue dans une boucle, on ne sait pas quelle variable de `starwars` est à l'origine du problème.
On va donc rajouter un `print(col)` comme première instruction de la boucle `for`.
```{r error=TRUE}
min_freq_values <- function(df, n_min) {
res <- list()
for (col in names(df)) {
print(col)
# Tri à plat des valeurs de la colonne
freq <- table(df[[col]])
# On conserve les modalités avec effectif >= n
freq <- freq[freq >= n_min]
# On ajoute la colonne au résultat s'il y a au moins une modalité
if (length(freq) > 0) res[[col]] <- freq
}
res
}
min_freq_values(starwars, 50)
```
Ce `print(col)` nous permet de voir que tout se passe bien pour les premières variables du tableau, et que l'erreur survient au moment de traiter la variable `films`. On regarde donc à quoi ressemble cette variable.
```{r}
head(starwars$films, 3)
```
*Damn*, cette variable n'est pas un vecteur atomique mais une liste ! Un tableau de données peut en effet contenir ce que l'on appelle des *colonnes-listes*. Comme ça n'est pas courant, on ne l'avait clairement pas prévu au moment de la création de la fonction.
On a désormais identifié le problème, on peut donc le corriger par exemple en ignorant les colonnes de type liste. Pour cela on utilise `is.list()` et l'instruction `next`.
```{r}
min_freq_values <- function(df, n_min) {
res <- list()
for (col in names(df)) {
# On passe à la colonne suivante si la colonne est une colonne-liste
if (is.list(df[[col]])) next
# Tri à plat des valeurs de la colonne
freq <- table(df[[col]])
# On conserve les modalités avec effectif >= n
freq <- freq[freq >= n_min]
# On ajoute la colonne au résultat s'il y a au moins une modalité
if (length(freq) > 0) res[[col]] <- freq
}
res
}
min_freq_values(starwars, 50)
```
Ça fonctionne ! Et cela nous permet de repérer au passage une "légère" sur-représentation des personnages masculins...
### browser()
Il existe des alternatives à `print()` plus efficaces pour identifier et résoudre des bugs. La plus utilisée est la fonction `browser()` : celle-ci peut s'insérer à n'importe quel endroit du code, et lorsqu'elle est rencontrée par R celui-ci s'interrompt et affiche une invite de commande permettant notamment d'inspecter des objets.
On reprend l'exemple précédent en remplaçant le `print(col)` que nous avions inséré pour débugguer la fonction `min_freq_values` par un appel à `browser()`.
```{r eval=FALSE}
min_freq_values <- function(df, n_min) {
res <- list()
for (col in names(df)) {
browser()
# Tri à plat des valeurs de la colonne
freq <- table(df[[col]])
# On conserve les modalités avec effectif >= n
freq <- freq[freq >= n_min]
# On ajoute la colonne au résultat s'il y a au moins une modalité
if (length(freq) > 0) res[[col]] <- freq
}
res
}
min_freq_values(starwars, 50)
```
Lorsqu'on lance ce code, R s'interrompt et affiche l'invite de commande suivant :
```
Called from: min_freq_values(starwars, 50)
Browse[1]>
```
Cette invite de commande offre plusieurs possibilités. La première est d'indiquer du code R qui sera exécuté dans le contexte au moment de l'interruption : si on indique un nom d'objet, on pourra donc afficher sa valeur. Ainsi, si on tape `col`, R nous affiche la valeur actuelle de `col`, donc au moment de la première itération de la boucle.
```
Browse[1]> col
[1] "name"
```
On peut également fournir des commandes spécifiques : si on tape `n`, R va passer à l'instruction suivante puis s'interrompre à nouveau^[Si on a un objet `n` dont on souhaite afficher le contenu, on doit faire `print(n)`.].
```
Browse[1]> n
debug à #6 : freq <- table(df[[col]])
```
Si on tape `c`, R va relancer l'exécution jusqu'à la fin, ou jusqu'à la prochaine rencontre d'un `browser()`. Dans notre cas, cela signifie qu'on va continuer jusqu'au `browser()` de la deuxième itération de la boucle.
```
Browse[2]> c
Called from: min_freq_values(starwars, 50)
Browse[1]> col
[1] "height"
```
Si on souhaite sortir de cette invite de commande et tout interrompre, il suffit de taper `Q`.
`browser()` est donc un peu plus complexe à utiliser que des `print()` ajoutés manuellement, mais c'est aussi un outil plus souple et plus puissant.
::: {.callout-note}
Cette section n'offre qu'un petit aperçu des possibilités de débuggage. R propose d'autres fonctionnalités, et les environnements de développement comme RStudio ou Visual Studio Code proposent également leurs propres outils.
Pour plus d'informations on pourra se reporter aux ressources en fin de chapitre.
:::
## benchmarking : mesurer et comparer les temps d'exécution
Lorsqu'on commence à créer ses propres fonctions et de manière générale à écrire du code de plus en plus complexe ou à travailler sur des données de plus en plus volumineuses, on peut arriver sur des problèmes de performance : les temps d'exécution deviennent longs et on aimerait essayer de les optimiser.
Il est alors parfois utile de faire du *benchmarking*, c'est-à-dire comparer plusieurs manières différentes de faire la même chose en mesurant leurs différences de vitesse d'exécution.
L'intruction la plus simple pour cela est sans doute `system.time()`. Celle-ci prend en argument une expression R, et affiche en retour le temps d'exécution en secondes.
```{r}
system.time(runif(1000000))
```
L'expression peut comporter plusieurs instructions, dans ce cas on les entoure d'accolades.
```{r}
system.time({
v <- runif(1000000)
moy <- mean(v)
})
```
`system.time()` est cependant très limitée : on ne peut exécuter qu'une expression à la fois, on ne peut donc pas en comparer plusieurs directement, et surtout le temps d'exécution peut varier assez sensiblement selon l'utilisation du processeur de la machine au moment où on lance la commande.
Il existe donc en complément plusieurs extensions dédiées au *benchmarking*. On va utiliser ici l'extension [bench](https://bench.r-lib.org/), installable avec :
```{r eval=FALSE}
install.packages("bench")
```
`bench` propose une fonction principale, nommée `mark()`, qu'on peut donc appeler directement avec `bench::mark()`^[L'avantage d'utiliser `bench::mark()` est qu'on n'a pas besoin d'ajouter un `library(bench)` dans notre script.]. On passe à cette fonction plusieurs expressions, et `bench::mark()` va effectuer les actions suivante :
- elle vérifie que les expressions retournent bien le même résultat (si on souhaite comparer des expressions qui renvoient des résultats différents, il faut ajouter l'argument `check = FALSE`)
- elle lance chacune de ces expressions plusieurs fois et mesure à chaque fois leur temps d'exécution
- elle affiche un résumé de ces temps d'exécution en indiquant notamment leur minimum et leur médiane
Exécuter chaque expression plusieurs fois permet de prendre en compte les fluctuations liées à l'activité du processeur de la machine. Par défaut, `bench::mark()` exécute chaque instruction au minimum une fois et au moins suffisamment de fois pour atteindre 0,5s d'exécution (ces valeurs peuvent être modifiées via les paramètres `min_iterations` et `min_time`).
Dans l'exemple suivant, on crée deux fonctions qui font la même chose : ajouter 10 à tous les éléments d'un vecteur passé en argument. Dans la première fonction `plus10_for` on utilise une boucle `for` qui ajoute 10 à tous les éléments l'un après l'autre (ce qu'il ne faut évidemment pas faire !), tandis que la deuxième fonction `plus10_vec` utilise la forme vectorisée `x + 10`.
```{r}
plus10_for <- function(x) {
for (i in seq_along(x)) {
x[i] <- x[i] + 10
}
x
}
plus10_vec <- function(x) {
x + 10
}
```
On lance un benchmark avec `bench::mark()` et on stocke le résultat dans un objet.
```{r}
x <- 1:10000
bnch <- bench::mark(
plus10_for(x),
plus10_vec(x)
)
```
On affiche les résultats obtenus :
```{r}
bnch
```
La colonne la plus importante est sans doute la colonne `median`, qui affiche le temps médian d'exécution des deux expressions. Attention, l'unité de temps n'est pas forcément la même, ici l'exécution de `plus10_for` est affichée en millisecondes ("ms"), tandis que celle de `plus10_vec` l'est en microsecondes ("µs") donc avec une unité mille fois plus petite.
Si on préfère, on peut afficher les performances relatives des deux fonctions avec :
```{r}
summary(bnch, relative = TRUE)
```
Ces résultats nous permettent de voir que la version `vec` est très nettement plus rapide que la version `for` ! Ce qui confirme le fait qu'il faut toujours prioriser l'utilisation de fonctions vectorisées lorsqu'elles existent.
On prend un second exemple : on souhaite mesurer si l'utilisation de `map` est plus rapide ou non qu'une boucle `for` pour une tâche équivalente.
Pour cela on commence par créer artificiellement une liste de 200 tableaux de données en dupliquant 100 fois une liste composée des tableaux `rp2018` et `hdv2003`.
```{r}
dfs <- list(rp2018, hdv2003)
dfs <- rep(dfs, 100)
```
Puis on lance un benchmark sur une boucle `for` et un `map` qui retournent chacun les nombres de ligne des 200 tableaux. Cette-fois on ne crée pas de fonctions : on passe le code directement à `bench::mark()`, avec des noms permettant de les identifier plus facilement dans les résultats.
```{r}
bench::mark(
boucle_for = {
res <- list()
for (df in dfs) {
res <- c(res, nrow(df))
}
res
},
map = map(dfs, ~ nrow(.x) )
)
```
La lecture du résultat indique que le `map` est environ 4 fois plus rapide que la boucle `for`.
Attention cependant en lisant ces résultats : dans la comparaison précédente les temps d'exécution étant en millisecondes, même une différence du type "4 fois plus rapide" peut être quasiment imperceptible. Dans l'exemple suivant on compare de la même manière `for` et `map`, mais cette fois en effectuant une action plus coûteuse en temps de calcul (on génère des vecteurs de nombres aléatoires).
```{r}
bench::mark(
boucle_for = {
res <- numeric()
for (i in 1:100) {
res <- c(res, mean(runif(50000)))
}
res
},
map = map_dbl(1:100, ~ mean(runif(50000))),
check = FALSE
)
```
Dans ce cas la différence entre `for` et `map` devient négligeable par rapport aux temps de calcul des opérations effectuées dans les boucles : au final les temps d'exécution sont presque identiques.
## Quelques conseils d'optimisation
### Privilégier les fonctions vectorisées
On l'a déjà dit, redit et reredit, une des forces de R est de proposer un grand nombre de fonctions vectorisées, c'est-à-dire prévues et optimisées pour s'appliquer à tous les éléments d'un vecteur. Quand une fonction vectorisée existe, il est donc toujours préférable de l'utiliser plutôt qu'une boucle ou un `map`.
```{r}
v <- rep(c("Pomme", "Poire", "Fraise"), 100)
bench::mark(
boucle = {
map_int(v, str_count, "m")
},
vec = str_count(v, "m")
)
```
### Mettre le minimum d'opérations dans les boucles
Si vous utilisez une boucle, toute opération qu'elle contient, comme elle va être répétée, peut devenir rapidement très coûteuse. Il ne faut donc y mettre que les opérations réellement nécessaires.
La fonction suivante prend en entrée un tableau de données `d` et une chaîne de caractères `chr`, et retourne un vecteur nommé indiquant le nombre de fois ou cette valeur apparaît dans chaque colonne du tableau. On a décidé de convertir en minuscules à la fois la valeur de `chr` et l'ensemble des colonnes du tableau pour que le comptage ne prenne pas en compte les différences de majuscules/minuscules.
```{r}
nb_chaine1 <- function(d, chr) {
res <- numeric()
for (var in names(d)) {
# Conversion de chr et des colonnes de d en minuscules
d <- d %>% modify(str_to_lower)
chr <- str_to_lower(chr)
# Comptage des occurrences de chr dans la colonne var
res[[var]] <- sum(d[[var]] == chr, na.rm = TRUE)
}
res
}
d <- hdv2003[, c("hard.rock", "qualif", "clso", "bricol")]
nb_chaine1(d, "oui")
```
Ça fonctionne, mais si on regarde un peu plus attentivement le code de la fonction on peut vite se rendre compte d'un problème : les conversions en minuscules sont faites à l'intérieur de la boucle, et sont donc répétées pour chaque colonne de `d`. Or ceci n'est absolument pas nécessaire puisque cette conversion ne dépend pas des colonnes et qu'elle peut être faite une seule et unique fois en début de fonction.
On décide donc de sortir les conversions en minuscules de la boucle.
```{r}
nb_chaine2 <- function(d, chr) {
res <- numeric()
# Conversion de chr et des colonnes de d en minuscules
d <- d %>% modify(str_to_lower)
chr <- str_to_lower(chr)
for (var in names(d)) {
# Comptage des occurrences de chr dans la colonne var
res[[var]] <- sum(d[[var]] == chr, na.rm = TRUE)
}
res
}
nb_chaine2(d, "oui")
```
Le résultat des deux fonctions est identique. On compare leur temps d'exécution à l'aide de `bench::mark()`.
```{r}
bench::mark(
nb_chaine1(d, "oui"),
nb_chaine2(d, "oui"),
)
```
Ce qui permet de constater que la deuxième version est quatre fois plus rapide (et elle sera d'autant plus rapide que le nombre de colonnes du tableau sera grand).
### Choisir un format de fichier adapté
Si on travaille sur des fichiers de données volumineux, les temps de chargement et de sauvegarde des données peuvent devenir importants.
Il existe différents formats de fichiers qui peuvont être plus ou moins rapides et pratiques selon l'utilisation qu'on en fait. Il n'est malheureusement pas toujours simple de s'y retrouver d'autant que les formats et les performances peuvent évoluer assez rapidement, mais on peut quand même donner quelques indications :
Le format **CSV** est pratique pour échanger des données tabulaires d'un système ou d'un logiciel à un autre, mais il présente plusieurs inconvénients :
- les fichiers sont volumineux
- les temps de lecture/écriture sont assez longs
- ils n'incluent pas de métadonnées comme les types des colonnes, et nécessitent donc des opérations de conversion plus ou moins "risquées"
À noter que plusieurs extensions et fonctions existent pour lire et/ou écrire les fichiers CSV. Outre `read.csv` de R base et `read_csv` de `readr`, on pourra mentionner `fread` de `data.table`, particulièrement rapide, ou l'extension `vroom`, qui ne charge pas toutes les informations d'un coup mais y accède "à la demande".
Les formats **Rdata** (utilisé par `load()` et `save()`) et **RDS** (utilisé par `readRDS()` et `saveRDS()`), propres à R, permettent de sauvegarder des objets dans un fichier. Ils ont des temps de lecture et d'écriture rapides, et permettent d'enregistrer n'importe quel objet R et d'être sûr de le retrouver à peu près à l'identique. À noter que ces formats permettent de faire varier le niveau de compression des données et donc jouer sur le rapport entre espace disque utilisé et temps d'accès. Ces formats sont intéressants notamment pour enregistrer des résultats intermédiaires. À noter que le format **fst**, proposé par [l'extension du même nom](https://www.fstpackage.org/), offre des performances encore supérieures mais ne gère que les données tabulaires.
Pour des données vraiment volumineuses, le package `arrow` permet la lecture et l'enregistrement dans différents formats, dont **arrow** et **parquet**. Très optimisés mais limités aux données tabulaires, ils permettent également des échanges avec d'autres langages de programmation comme Python, JavaScript, etc.
À titre indicatif, le billet [a shallow benchmark of R data frame export/import methods](https://data.nozav.org/post/2019-r-data-frame-benchmark/) propose un comparatif entre les performances de différents formats de fichiers pour des données tabulaires. Attention cependant, il date de 2019 et les performances des fonctions testées ont pu évoluer depuis.
### Repérer les opérations coûteuses avec du *profiling*
Quand on commence à écrire du code un peu long, il n'est pas toujours évident de savoir quelles sont les opérations qui sont les plus coûteuses en termes de performance. Il peut alors être utile d'utiliser un outil de *profiling*, qui consiste à exécuter du code tout en mesurant les temps d'exécution de chaque instruction, ce qui permet ensuite de visualiser et repérer les opérations qui prennent le plus de temps et qui seraient donc les plus intéressantes à optimiser (si c'est possible).
L'extension `profvis` fournit un outil de *profiling* sous R pratique et utile. On passe des instructions ou un script entier à la fonction `profvis()`, et celle-ci fournit une visualisation interactive des différentes opérations, de leur temps d'exécution, de leur utilisation de la mémoire...
![Exemple de visualisation générée par profvis()](resources/screenshots/profvis.png)
L'utilisation de `profvis()` dépasse le cadre de ce document, mais on pourra trouver des explications (en anglais) dans la partie [profiling](https://adv-r.hadley.nz/perf-measure.html#profiling) d'Advanced R.
### Mettre en cache des résultats intermédiaires
Selon la taille des données et le type d'opérations réalisées, les temps de calculs même optimisés peuvent demeurer longs. Dans ce cas il peut être intéressant de "mettre en cache" des résultats intermédiaires dans des fichiers pour pouvoir les charger directement sans avoir à tout recalculer.
Par exemple, si on travaille sur un corpus de données textuelles, on peut enregistrer dans un fichier `RDS` ou `Rdata` le résultat de toutes les opérations de prétraitement du corpus : si on souhaite faire des analyses par la suite on aura juste à charger les données depuis ce fichier sans avoir à relancer le calcul de tous les prétraitements.
L'extension `targets`, présentée @sec-targets, est particulièrement adaptée pour les projets de ce type car elle gère automatiquement les dépendances entre les étapes d'un projet et la mise en cache des résultats, et permet ainsi d'optimiser les temps de traitement.
### Utiliser d'autres extensions
Enfin, si ce document est basé sur les extensions du *tidyverse*, qui offrent une syntaxe cohérente et plus facile d'accès, il existe d'autres extensions notamment dans le domaine de la manipulation des données qui permettent des opérations plus rapides.
C'est en particulier le cas de l'extension `data.table`, qui utilise une syntaxe moins accessible que celle de `dplyr`, mais propose des performances en général [assez nettement supérieures](https://h2oai.github.io/db-benchmark/). On trouvera la documentation détaillée de `data.table` (en anglais) [sur le site de l'extension](https://rdatatable.gitlab.io/data.table/).
## Ressources
L'ouvrage *Advanced R* (en anglais) consacre un chapitre entier [au debuggage](https://adv-r.hadley.nz/debugging.html), un autre à [la mesure de la performance](https://adv-r.hadley.nz/perf-measure.html), et un dernier à différentes techniques d'[optimisation du code](https://adv-r.hadley.nz/perf-improve.html).
Le site de RStudio propose une page entière détaillant ses [fonctionnalités de debugging](https://support.rstudio.com/hc/en-us/articles/205612627-Debugging-with-RStudio) (en anglais). Pour les utilisateurs de Visual Studio Code, on pourra se référer à l'extension [VSCode-R-Debugger](https://github.com/ManuelHentschel/VSCode-R-Debugger).