Analyse comparative (benchmark) de data.table
2025-01-21
Source:vignettes/fr/datatable-benchmarking.Rmd
datatable-benchmarking.Rmd
Ce document a pour but de guider la mesure de la performance de
data.table
. Il centralise la documentation des meilleures
pratiques et des pièges à éviter.
fread : effacer les caches
Idéalement, chaque appel à fread
devrait être exécuté
dans une nouvelle session avec les commandes suivantes précédant
l’exécution de R. Cela permet d’effacer le fichier cache du système
d’exploitation en RAM et le cache du disque dur.
free -g
sudo sh -c 'echo 3 >/proc/sys/vm/drop_caches'
sudo lshw -class disk
sudo hdparm -t /dev/sda
Lorsque l’on compare fread
à des solutions non-R, il
faut savoir que R exige que les valeurs des colonnes de caractères
soient ajoutées au cache global de chaînes de caractères de R.
Cela prend du temps lors de la lecture des données, mais les opérations
ultérieures en bénéficient puisque les chaînes de caractères ont déjà
été mises en cache. Par conséquent, en plus de chronométrer des tâches
isolées (comme fread
seul), c’est une bonne idée d’évaluer
le temps total d’un pipeline de traitement de données de bout en bout
contenant des tâches telles que la lecture de données, leur manipulation
et la production de la sortie finale.
sous-ensemble : seuil d’optimisation de l’index pour les requêtes composées
L’optimisation de l’index pour les requêtes de filtres composés ne sera pas utilisée lorsque le produit croisé des éléments fournis au filtre dépasse 1e4 éléments.
DT = data.table(V1=1:10, V2=1:10, V3=1:10, V4=1:10)
setindex(DT)
v = c(1L, rep(11L, 9))
length(v)^4 # produit en croix des éléments du filtre
#[1] 10000 # <= 10000
DT[V1 %in% v & V2 %in% v & V3 %in% v & V4 %in% v, verbose=TRUE]
#Optimisation du sous-ensemble avec l'index 'V1__V2__V3__V4'
#on= correspond à l'index existant, utilise l'index
#Démarrage de bmerge ...terminé en 0.000sec
#...
v = c(1L, rep(11L, 10))
length(v)^4 # produit croisé des éléments du filtre
#[1] 14641 # > 10000
DT[V1 %in% v & V2 %in% v & V3 %in% v & V4 %in% v, verbose=TRUE]
#Optimisation de la substitution désactivée car le produit croisé des valeurs du membre droit dépasse 1e4, ce qui cause des problèmes de mémoire.
#...
sous-ensemble : analyse comparative basée sur l’index
Pour des raisons de commodité, data.table
construit
automatiquement un index sur les champs que vous utilisez pour
sous-répertorier les données. Cela ajoutera une certaine surcharge au
premier sous-ensemble sur des champs particuliers, mais réduira
considérablement le temps d’interrogation de ces colonnes dans les
exécutions suivantes. La meilleure façon de mesurer la vitesse est de
mesurer séparément la création d’un index et les requêtes utilisant un
index. Avec de tels temps, il est facile de décider quelle est la
stratégie optimale pour votre cas d’utilisation. Pour contrôler
l’utilisation de l’index, employez les options suivantes :
-
use.index=FALSE
forcera la requête à ne pas utiliser les index même s’ils existent, mais les clés existantes sont toujours utilisées pour l’optimisation. -
auto.index=FALSE
désactive la construction automatique d’index lors d’un sous-ensemble sur des données non indexées, mais si les index ont été créés avant que cette option ne soit définie, ou explicitement en appelantsetindex
, ils seront toujours utilisés pour l’optimisation.
Deux autres options permettent de contrôler l’optimisation de manière globale, y compris l’utilisation d’index :
options(datatable.optimize=2L)
désactivera complètement
l’optimisation des sous-ensembles, tandis que
options(datatable.optimize=3L)
la réactivera. Ces options
affectent beaucoup plus d’optimisations et ne devraient donc pas être
utilisées lorsque seul le contrôle des index est nécessaire. Plus
d’informations dans ?datatable.optimize
.
opérations par référence
Lors de l’évaluation des fonctions set*
, il n’est utile
de mesurer que la première exécution. Ces fonctions mettent à jour leur
entrée par référence, donc les exécutions suivantes utiliseront le
fichier data.table
déjà traité, ce qui faussera les
résultats.
Protéger votre data.table
d’une mise à jour par des
opérations de référence peut être réalisé en utilisant les fonctions
copy
ou data.table:::shallow
. Soyez conscient
que copy
peut être très coûteux car il doit dupliquer
l’objet entier. Il est peu probable que nous voulions inclure le temps
de duplication dans le temps de la tâche réelle que nous
benchmarkons.
tenter d’étalonner les processus atomiques
Si votre analyse comparative est destinée à être publiée, elle sera beaucoup plus utile si vous la divisez pour mesurer la durée des processus atomiques. De cette manière, vos lecteurs pourront voir combien de temps a été consacré à la lecture des données à partir de la source, au nettoyage, à la transformation proprement dite et à l’exportation des résultats. Bien sûr, si votre benchmark est destiné à présenter un flux de travail de bout en bout, il est tout à fait logique de présenter le temps global. Néanmoins, la séparation des temps des étapes individuelles est utile pour comprendre quelles étapes sont les principaux goulots d’étranglement d’un flux de travail. Il existe d’autres cas où le benchmarking atomique n’est pas souhaitable, par exemple lors de la lecture d’un csv, suivie d’un regroupement. R nécessite de remplir le cache global de chaînes de caractères de R, ce qui ajoute une surcharge supplémentaire lors de l’importation de données de caractères dans une session R. D’un autre côté, le cache global de chaînes de caractères peut accélérer des processus tels que le regroupement. Dans de tels cas, lorsque l’on compare R à d’autres langages, il peut être utile d’inclure le temps total.
éviter la coercition de classe
Si ce n’est pas ce que vous voulez vraiment mesurer, vous devez préparer des objets d’entrée de la classe attendue pour chaque outil que vous comparez.
éviter microbenchmark(..., times=100)
Répéter un benchmark plusieurs fois ne donne généralement pas l’image la plus claire des outils de traitement des données. Bien sûr, c’est parfaitement logique pour les calculs plus atomiques, mais ce n’est pas une bonne représentation de la manière la plus courante dont ces outils seront utilisés, à savoir pour les tâches de traitement des données, qui consistent en des lots de transformations fournies de manière séquentielle, chacune exécutée une fois. Matt a dit un jour :
Je me méfie beaucoup des benchmarks qui prennent moins d’une seconde. Je préfère de loin 10 secondes ou plus pour une seule exécution, obtenues en augmentant la taille des données. Un nombre de répétitions de 500 tire la sonnette d’alarme. 3 à 5 exécutions devraient suffire à convaincre sur des données plus importantes. Le coût des appels de fonctions et le temps nécessaire au GC affectent les calculs à une si petite échelle.
Ceci est tout à fait vrai. Plus la mesure du temps est petite, plus le bruit est important, de manière relative. Le bruit est généré par le dispatching des méthodes, l’initialisation de packages/classes, etc. Le benchmark devrait se concentrer sur des scénarios d’utilisation réelle.
traitement multithread
L’un des principaux facteurs susceptibles d’influer les délais
d’exécution est le nombre de threads disponibles dans votre session R.
Dans les versions récentes de data.table
, certaines
fonctions sont parallélisées. Vous pouvez contrôler le nombre de threads
que vous voulez utiliser avec setDTthreads
.
setDTthreads(0) # utilise tous les cœurs disponibles (par défaut)
getDTthreads() # vérifie combien de cœurs sont actuellement utilisés
à l’intérieur d’une boucle, préférez set
au lieu de
:=
À moins que vous n’utilisiez l’index en faisant un
sous-affectation par référence, vous devriez préférer la
fonction set
qui n’impose pas la surcharge de l’appel à la
méthode [.data.table
.
DT = data.table(a=3:1, b=lettres[1:3])
setindex(DT, a)
# for (...) { # imaginez une boucle ici
DT[a==2L, b := "z"] # sous-affectation par référence, utilise l'index
DT[, d := "z"] # pas de sous-affectation par référence, n'utilise pas l'index et ajoute la surcharge de `[.data.table`
set(DT, j="d", value="z") # pas de surcharge `[.data.table`, mais pas encore d'index, jusqu'à #1196
# }
à l’intérieur d’une boucle, préférez setDT
au lieu de
data.table()
Pour l’instant, data.table()
a un surcoût, donc à
l’intérieur des boucles, il est préférable d’utiliser
as.data.table()
ou setDT()
sur une liste
valide.