Passer R à l’échelle : vectoriser, hors-mémoire et parallélisation

Repérez le code lent et choisissez la bonne solution — vectoriser d’abord, passer hors-mémoire quand les données dépassent la RAM, paralléliser quand c’est limité par le CPU

Votre code R est trop lent sur un grand jeu de données — quelle solution choisir ? Cette leçon enseigne l’arc de décision pour passer à l’échelle, benchmarks exécutables à l’appui : vectoriser d’abord, passer aux moteurs hors-mémoire (Apache Arrow et DuckDB) quand les données dépassent la RAM, et paralléliser avec les packages future et furrr quand le travail est réellement limité par le CPU.

Date de publication

19 juillet 2026

Modifié

19 juillet 2026

AstucePoints clés
  • Procédez dans l’ordre. Vectorisez d’abord (réécrivez la boucle pour la supprimer), passez hors-mémoire (Apache Arrow / DuckDB) quand les données ne tiennent plus en RAM, et parallélisez (future / furrr) seulement quand le travail est réellement limité par le CPU. La plupart du « R lent » vient d’une vectorisation manquante, pas d’un cœur manquant.
  • Vectoriser rapporte le plus, gratuitement. Une boucle qui fait de l’arithmétique sur un vecteur entier un élément à la fois peut être plusieurs ordres de grandeur plus lente que la ligne vectorisée — et la version vectorisée est plus courte et plus claire.
  • Le hors-mémoire bat « achetez plus de RAM ». Arrow et DuckDB poussent la requête dans un moteur C++ et ne lisent que les colonnes et les lignes dont ils ont besoin, si bien que vous résumez un fichier bien plus grand que la mémoire sans le charger.
  • Le parallèle est un dernier recours, pas un premier réflexe. Lancer des workers a un coût fixe ; cela ne paie que lorsque chaque bloc indépendant est lourd. Le future_map() de furrr remplace directement purrr::map() — changez le plan() et le même code s’exécute sur plusieurs cœurs.
  • C’est le jugement qu’une IA ne peut pas simuler. Un copilote écrit volontiers la boucle for lente ; votre valeur durable, c’est de repérer qu’elle est lente et de savoir laquelle de ces trois solutions elle appelle.

Introduction

Votre analyse marchait très bien sur un échantillon. Puis vous l’avez lancée sur les vraies données — quelques millions de lignes — et R « réfléchit » depuis dix minutes. C’est le moment que rencontre tout utilisateur de R, et c’est moins un problème de syntaxe qu’un problème de jugement : la solution dépend entièrement de pourquoi le code est lent, et se tromper d’outil gâche un après-midi.

Il y a trois solutions, et elles s’appliquent dans un ordre fixe :

  1. Vectoriser — remplacer le parcours élément par élément par des opérations sur l’objet entier. C’est là que se cache le plus de vitesse, et cela ne coûte qu’une réécriture.
  2. Passer hors-mémoire — quand les données sont plus grandes que la mémoire (elles ne se chargent même pas), cessez d’essayer de les tenir en RAM et laissez un moteur hors-cœur (Apache Arrow ou DuckDB) faire le travail sur disque.
  3. Paralléliser — quand le travail est réellement limité par le CPU (cœurs occupés, données tenant en mémoire, et chaque morceau indépendant), répartissez-le sur plusieurs cœurs avec future et furrr.

Un peu de vocabulaire, car ces termes sont employés de façon approximative. La vectorisation consiste à exprimer un calcul comme une opération sur un vecteur ou une colonne entière d’un coup, de sorte que la boucle s’exécute en C compilé plutôt qu’en R interprété. Plus grand que la mémoire (ou hors-cœur) signifie que le jeu de données est trop volumineux pour tenir en RAM ; les moteurs hors-mémoire le traitent par morceaux depuis le disque. Limité par le CPU signifie que le goulot d’étranglement est le calcul (les cœurs sont bloqués à 100 %), par opposition à limité par la mémoire (vous avez épuisé la RAM) ou limité par les E/S (en attente du disque ou du réseau).

C’est exactement la compétence qu’un copilote IA n’a pas. Demandez-lui de « calculer le chiffre d’affaires par ligne » et il vous tendra allègrement une boucle for — correcte, et souvent discrètement quadratique. Le chatbot écrit la version naïve ; reconnaître qu’elle est lente, et savoir si la réponse est de vectoriser, d’aller sur le disque ou d’ajouter des cœurs, c’est la part qui reste la vôtre. Nous allons donc tout mesurer : ce sont les chiffres, pas les impressions, qui décident.

NoteExécutez ces blocs dans l’ordre

Cette leçon construit un jeu de données synthétique au départ et le réutilise (ainsi qu’un fichier écrit à partir de lui) dans chaque benchmark suivant, de sorte que les comparaisons portent toutes sur les mêmes données. Copiez les blocs de haut en bas. Les mesures que vous verrez différeront des miennes — elles dépendent de votre machine — mais c’est la forme du résultat (quelle approche est plus rapide, et d’à peu près combien) qui compte.

Un jeu de données assez grand pour le sentir

Nous utiliserons une table de ventes synthétique de cinq millions de lignes — assez petite pour se construire en une seconde, assez grande pour que le code bâclé fasse mal. Chaque ligne est une commande : une région, un nombre d’unités et un prix unitaire. set.seed() la rend reproductible.

library(bench)
set.seed(2026)

n <- 5e6
sales <- data.frame(
  region = sample(c("North", "South", "East", "West"), n, replace = TRUE),
  units  = sample(1:20, n, replace = TRUE),
  price  = round(runif(n, 5, 100), 2)
)

nrow(sales)
[1] 5000000
head(sales, 3)
  region units price
1  North    15 19.86
2  North     8 52.31
3  North     8 71.72

Cinq millions de lignes, trois colonnes. La tâche tout du long : calculer le chiffre d’affaires de chaque commande (units * price) et le totaliser par région — un calcul trivial qui devient un test de résistance à grande échelle.

Vectoriser d’abord — le plus gros gain, gratuitement

La façon instinctive de calculer le chiffre d’affaires est de boucler sur les lignes. L’instinct est bon ; l’implémentation, généralement, ne l’est pas. Voici trois versions du même calcul, et les écarts entre elles sont énormes.

La première fait croître un vecteur de résultat un élément à la fois avec c(). C’est le motif qu’écrit un copilote IA (et la plupart d’entre nous, une fois) — et c’est un piège : chaque c() réalloue et copie tout le vecteur en croissance, si bien que le coût croît comme le carré de la longueur. La deuxième pré-alloue le résultat et le remplit par indice — une hygiène de boucle correcte. La troisième est vectorisée : R multiplie les deux colonnes en une seule opération compilée, sans aucune boucle au niveau R.

Nous mesurons sur vingt mille lignes (la boucle en croissance est trop lente à chronométrer sur cinq millions) avec bench::mark(), qui chronomètre chaque expression et, par défaut, vérifie qu’elles renvoient des résultats identiques — nous savons donc que nous comparons trois routes vers la même réponse :

u <- sales$units[1:2e4]
p <- sales$price[1:2e4]

grow_loop <- function(u, p) {
  out <- c()                              # start empty
  for (i in seq_along(u)) out <- c(out, u[i] * p[i])   # grow — copies every step
  out
}
prealloc_loop <- function(u, p) {
  out <- numeric(length(u))               # allocate once
  for (i in seq_along(u)) out[i] <- u[i] * p[i]
  out
}
vectorized <- function(u, p) u * p        # one compiled operation

res <- bench::mark(
  grow     = grow_loop(u, p),
  prealloc = prealloc_loop(u, p),
  vector   = vectorized(u, p),
  min_iterations = 3
)
res[, c("expression", "median", "mem_alloc")]
# A tibble: 3 × 3
  expression   median mem_alloc
  <bch:expr> <bch:tm> <bch:byt>
1 grow          380ms    1.49GB
2 prealloc    546.4µs  179.06KB
3 vector       23.7µs   156.3KB

Lisez la colonne median. La boucle en croissance est de loin la plus lente et alloue le plus de mémoire (toutes ces copies) ; la pré-allocation récupère l’essentiel de la perte ; et la ligne vectorisée est plus rapide encore — souvent de loin — tout en n’allouant presque rien. Une image rend l’écart évident :

library(ggplot2)

timing <- data.frame(
  method  = c("grow (c() in loop)", "pre-allocated loop", "vectorized (u * p)"),
  seconds = as.numeric(res$median)
)

# Lollipop, not bars: on a log axis a bar has no meaningful zero baseline, so encode
# time by POSITION (the point) — the fastest method sits farthest left / lowest.
ggplot(timing, aes(x = reorder(method, seconds), y = seconds)) +
  geom_segment(aes(xend = reorder(method, seconds), yend = min(seconds)),
               color = "#9db8d2", linewidth = 1) +
  geom_point(size = 4, color = "#3a86d4") +
  scale_y_log10() +
  coord_flip() +
  labs(x = NULL, y = "Median time (seconds, log scale — lower is faster)",
       title = "Same result, wildly different cost") +
  theme_minimal(base_size = 13)
Horizontal lollipop chart of median execution time on a log scale (lower and further left is faster) for three ways to compute revenue on 20,000 rows: the growing for-loop sits far to the right as by far the slowest, the pre-allocated loop is much faster, and the vectorized one-liner is farthest left as the fastest.
Figure 1

Et comme la forme vectorisée n’a aucune boucle au niveau R, elle passe sans effort à l’échelle des cinq millions de lignes complètes — le calcul que la boucle en croissance serait encore en train de mouliner :

system.time(sales$revenue <- sales$units * sales$price)
   user  system elapsed 
  0.006   0.027   0.038 

La vectorisation ne concerne pas que l’arithmétique. Le même principe — opérer sur le vecteur entier, laisser le code C de R faire la boucle — couvre la plupart du travail quotidien :

# Vectorized conditional: no loop, no if/else
sales$tier <- ifelse(sales$revenue > 1000, "large", "standard")

# Logical subsetting instead of looping to filter
big <- sales[sales$revenue > 1500, ]
nrow(big)
[1] 212811
# Built-in vectorized aggregations beat apply(margin) and loops
m <- as.matrix(sales[1:1000, c("units", "price", "revenue")])
colMeans(m)          # one call, whole columns
    units     price   revenue 
 10.50200  53.34367 558.72991 

La règle empirique : si vous écrivez une boucle for dont le corps touche un élément à la fois, demandez-vous si une fonction vectorisée ne fait pas déjà tout le vecteur d’un coup. ifelse(), l’indexation logique, colMeans()/rowSums(), cumsum(), pmin()/pmax() et tapply() couvrent une énorme fraction des boucles réelles. Vectorisez avant même de penser aux cœurs ou au disque — c’est le gain le moins cher et le plus grand, et souvent c’est la seule solution dont vous avez besoin.

Passer hors-mémoire quand les données ne tiennent pas

Vectoriser suppose que les données sont dans R. Mais les vrais jeux de données ne le sont souvent pas : quelques années de transactions font des dizaines de gigaoctets, plus que la RAM d’un portable, et read.csv() meurt avant même que votre code ne s’exécute. Acheter plus de mémoire n’est pas une stratégie. Le bon geste est d’arrêter de charger le tout et de laisser un moteur hors-mémoire (hors-cœur) faire le travail sur disque, en ne lisant en flux que ce dont il a besoin.

Deux outils dominent cet espace dans R, et tous deux vous laissent continuer à écrire du code familier.

D’abord, nous écrirons notre table au format Parquet — un format de fichier compressé et en colonnes que ces moteurs lisent efficacement (il stocke chaque colonne séparément, si bien qu’une requête touchant deux colonnes ne lit jamais le reste). Nous l’écrivons dans un répertoire temporaire pour que rien n’encombre votre projet :

library(arrow)

ds_path <- file.path(tempdir(), "sales_ds")
write_dataset(sales, ds_path, partitioning = "region", format = "parquet")

list.files(ds_path)   # one folder per region — partitioned on disk
[1] "region=East"  "region=North" "region=South" "region=West" 

Apache Arrow — un backend dplyr sur des fichiers sur disque

Apache Arrow donne à dplyr un backend sur disque. open_dataset() pointe vers les fichiers mais ne lit rien encore — il est paresseux : vous empilez des verbes dplyr, et le travail n’a lieu que lorsque vous appelez collect(), moment où Arrow ne lit que les colonnes et les lignes dont la requête a besoin (c’est le projection and predicate pushdown) et renvoie un petit résultat. Vous pouvez résumer ainsi un jeu de données de cent gigaoctets en touchant à peine la RAM. Le guide Arrow « Analyzing multi-file datasets » documente le motif :

library(dplyr)

ds <- open_dataset(ds_path)      # lazy: no data read yet

ds |>
  group_by(region) |>
  summarise(total = sum(revenue), orders = n()) |>
  arrange(desc(total)) |>
  collect()                      # NOW it runs, and returns only the summary
# A tibble: 4 × 3
  region      total  orders
  <chr>       <dbl>   <int>
1 North  689886844. 1250207
2 East   689577238. 1250818
3 West   689176000. 1250658
4 South  688001886. 1248317

Le code se lit exactement comme un pipeline dplyr en mémoire — c’est tout l’intérêt. Rien dans l’analyse n’a changé ; seul le moteur en dessous l’a fait. Arrow ne lit que les colonnes region et revenue que touche cette requête et passe le reste en flux, ce qui explique qu’il reste bon marché sur des fichiers bien plus grands que la mémoire. (Notez que la règle du pilier base-R fléchit ici volontairement : Arrow et DuckDB sont le backend dplyr, donc dplyr est leur idiome natif, pas un choix de style.)

DuckDB — un moteur SQL qui parle R

DuckDB est une base de données analytique en-processus — pensez à « SQLite pour l’analytique » — qui s’exécute dans votre session R sans rien à installer ni configurer. Il peut interroger un data frame que vous avez déjà, ou lire des fichiers Parquet et CSV directement depuis le disque sans les charger dans R.

Pour interroger un data frame en mémoire, enregistrez-le comme table virtuelle (duckdb_register() partage les données sans les copier) et lancez du SQL :

library(duckdb)
library(DBI)

con <- dbConnect(duckdb())
duckdb_register(con, "sales", sales)     # zero-copy view of the R data frame

dbGetQuery(con, "
  SELECT region, SUM(revenue) AS total, COUNT(*) AS orders
  FROM sales
  GROUP BY region
  ORDER BY total DESC
")
  region     total  orders
1  North 689886844 1250207
2   East 689577238 1250818
3   West 689176000 1250658
4  South 688001886 1248317

L’astuce la plus puissante consiste à pointer DuckDB directement vers les fichiers, de sorte que R ne les charge jamais du tout — le vrai cas plus-grand-que-la-mémoire. Le read_parquet() de DuckDB lit le jeu de données depuis le disque, fait le regroupement dans son moteur C++, et ne rend que le résumé de quatre lignes :

glob <- file.path(ds_path, "**", "*.parquet")

dbGetQuery(con, sprintf("
  SELECT region, SUM(revenue) AS total, COUNT(*) AS orders
  FROM read_parquet('%s')
  GROUP BY region
  ORDER BY total DESC
", glob))
  region     total  orders
1  North 689886844 1250207
2   East 689577238 1250818
3   West 689176000 1250658
4  South 688001886 1248317
dbDisconnect(con, shutdown = TRUE)       # always close the connection

Vous préférez dplyr au SQL ? DuckDB fonctionne aussi comme backend dbplyrtbl(con, "sales") |> group_by(region) |> summarise(...) — vous pouvez donc piloter le même moteur avec des verbes dplyr et le laisser traduire en SQL. Arrow ou DuckDB, dplyr ou SQL : le geste décisif est le même — pousser le calcul vers un moteur qui lit depuis le disque, au lieu de tirer d’abord des gigaoctets dans R.

Paralléliser quand c’est réellement limité par le CPU

Vous avez vectorisé, et les données tiennent en mémoire — mais le travail reste lent parce qu’il fait beaucoup de calcul : ajuster un modèle par groupe, lancer une simulation, bootstrapper des milliers de rééchantillonnages. C’est du travail limité par le CPU, et si les morceaux sont indépendants (parallélisation triviale — aucun morceau n’a besoin du résultat d’un autre), vous pouvez les exécuter sur plusieurs cœurs à la fois.

Commencez par voir ce dont vous disposez. Base R fournit le package parallel ; detectCores() indique les cœurs physiques, et availableCores() de future est le nombre le plus honnête (il respecte les limites de conteneur et les allocations de l’ordonnanceur) :

library(future)
parallel::detectCores()
[1] 10
availableCores()
system 
    10 

La façon moderne et ergonomique d’utiliser ces cœurs est le framework future avec furrr. Deux idées le portent :

  • plan() définit comment le travail s’exécute — plan(sequential) (par défaut, un seul cœur) ou plan(multisession, workers = k) (k sessions R en arrière-plan sur cette machine). Vous changez le plan ; vous ne changez pas votre code.
  • future_map() est un remplacement direct de purrr::map() : mêmes entrées, même sortie, mais il confie les itérations au plan() actif, quel qu’il soit. future_map_dbl(), future_map2() et leurs semblables reflètent le reste de la famille purrr.

Voici une tâche délibérément gourmande en CPU par élément — un petit bootstrap d’une pente de régression — exécutée sur huit éléments. Comme la tâche tire des nombres aléatoires, furrr a besoin de furrr_options(seed = TRUE) pour générer des flux aléatoires sûrs en parallèle et reproductibles (sautez-le et vous obtenez un avertissement et des résultats non reproductibles). Nous la chronométrons en séquentiel, puis sur plusieurs cœurs, en changeant seulement le plan() :

library(furrr)

boot_slope <- function(seed) {
  x <- rnorm(1e5)
  y <- 2 * x + rnorm(1e5)
  mean(replicate(25, {
    i <- sample.int(length(x), replace = TRUE)
    coef(lm(y[i] ~ x[i]))[[2]]
  }))
}

plan(sequential)                         # one core
system.time(
  seq_res <- future_map_dbl(1:8, boot_slope, .options = furrr_options(seed = TRUE))
)
   user  system elapsed 
  2.603   0.283   2.926 
plan(multisession, workers = min(4L, availableCores()))   # several cores
system.time(
  par_res <- future_map_dbl(1:8, boot_slope, .options = furrr_options(seed = TRUE))
)
   user  system elapsed 
  0.075   0.010   1.195 
plan(sequential)                         # always reset the plan

Le run parallèle devrait se terminer en un temps d’horloge nettement moindre. Deux réserves honnêtes, cependant : l’accélération est bornée par votre nombre de cœurs (quatre workers ne peuvent pas battre 4×) et entamée par le coût fixe — lancer les sessions worker et leur expédier les données coûte une fraction de seconde. C’est pourquoi le parallèle est le dernier levier : sur une tâche déjà rapide, ou que vous auriez pu vectoriser, le coût fixe rend le code parallèle plus lent. Ne le mobilisez que lorsque chaque bloc est réellement lourd et indépendant.

NoteEt foreach / parallel ?

Vous croiserez foreach() avec doParallel, et les mclapply()/parLapply() de base R, dans beaucoup de code existant — ils parallélisent de la même façon et restent parfaitement bons. La pile future/furrr en est l’équivalent moderne : un seul plan() bascule entre le séquentiel, plusieurs sessions locales, et même des clusters distants sans réécrire la boucle, et future_map() conserve les valeurs de retour à type stable de purrr. Le code neuf est plus facile à appréhender avec furrr ; pas besoin d’arracher un foreach qui fonctionne.

La décision, en une image

Quand le code est lent, parcourez l’arc de haut en bas — le premier « oui » est presque toujours la bonne solution, et vous avez rarement besoin de celle d’en dessous :

flowchart TD
    A[Code R trop lent] --> B{Boucle pour un travail<br/>sur vecteur entier ?}
    B -->|Oui| V[Vectoriser<br/>supprimer la boucle]
    B -->|Non| C{Données plus grandes<br/>que la RAM ?}
    C -->|Oui| M[Passer hors-mémoire<br/>Arrow / DuckDB]
    C -->|Non| D{Limité par le CPU avec<br/>blocs indépendants ?}
    D -->|Oui| P[Paralléliser<br/>future / furrr]
    D -->|Non| E[Profilez d'abord —<br/>le goulot est ailleurs]

L’ordre n’est pas arbitraire. Vectoriser est gratuit et généralement le plus gros gain, donc cela vient en premier. Passer hors-mémoire est la réponse à un problème de mémoire, pas de vitesse — utilisez-le quand les données ne tiennent pas, pas pour rendre de petites données plus rapides. Paralléliser dépense des cœurs (et un coût fixe) pour gagner du temps, donc c’est en dernier, et seulement pour du travail lourd et indépendant. Et si aucune des trois ne s’applique, le geste honnête est de profiler (utils::Rprof(), le package profvis) et de trouver le vrai goulot d’étranglement avant d’optimiser la mauvaise ligne.

Problèmes fréquents

Votre code fait croître silencieusement un objet dans une boucle. out <- c(out, x), df <- rbind(df, row), ou result[[length(result) + 1]] <- x à l’intérieur d’une boucle for réallouent et copient à chaque itération — le piège quadratique du benchmark ci-dessus. Soit pré-allouez à la taille finale et remplissez par indice, soit (mieux) vectorisez le tout pour le supprimer. C’est la cause la plus fréquente de « R est lent », et c’est exactement le code qu’un copilote IA a tendance à produire — mesurez tout ce qui est généré avant de lui faire confiance à grande échelle.

Vous avez parallélisé et c’est devenu plus lent. Les workers parallèles coûtent une mise en place fixe (démarrer des sessions R, leur copier les données). Si chaque tâche est courte, ce coût fixe écrase le calcul et la version séquentielle l’emporte. Solution : réservez le parallélisme aux blocs réellement lourds et indépendants, et confirmez avec un rapide system.time() que le run parallèle est effectivement plus rapide sur vos données avant de vous y engager.

Les nombres aléatoires dans future_map() ne sont pas reproductibles (ou lèvent un avertissement). Un set.seed() ordinaire ne se propage pas aux workers parallèles en toute sûreté. Passez .options = furrr_options(seed = TRUE) (ou un entier fixe) pour que furrr génère des flux RNG indépendants et reproductibles par worker. Sans cela, les résultats changent d’un run à l’autre et furrr vous avertit.

Vous avez chargé tout le fichier, puis filtré. read.csv() (ou arrow::read_parquet() dans un data frame) suivi d’un filter() paie quand même la lecture de tout en RAM. Avec Arrow ou DuckDB, gardez-le paresseux : open_dataset() / tbl(), appliquez le filtre et le résumé, et collect() en dernier — pour que le moteur ne lise que les lignes et les colonnes dont la requête a besoin.

Questions fréquentes

Parcourez l’arc dans l’ordre. D’abord vectorisez — remplacez les boucles élément par élément (et tout objet qui croît avec c()/rbind() dans une boucle) par des opérations sur le vecteur entier comme u * p, ifelse() ou colMeans() ; c’est généralement le plus gros gain et cela ne coûte rien. Si les données sont trop grandes pour la RAM, passez à un moteur hors-mémoire (Arrow ou DuckDB). Si le travail est réellement limité par le CPU et que ses morceaux sont indépendants, parallélisez avec future/furrr. Mesurez avec bench::mark() pour que les chiffres, pas les suppositions, vous guident.

R est rapide quand vous l’utilisez comme il est conçu pour l’être — sur des vecteurs et des colonnes entières, où la boucle s’exécute en C compilé. La plupart des expériences de « R est lent » sont en fait des boucles au niveau R interprété (surtout les objets qui croissent à chaque itération) que la vectorisation corrige d’emblée. Pour des données plus grandes que la mémoire, les moteurs hors-cœur comme DuckDB et Arrow sont réellement rapides ; pour du calcul lourd et indépendant, R parallélise sur plusieurs cœurs aussi bien que n’importe quel langage.

Les deux exécutent des itérations indépendantes en parallèle et les deux conviennent. furrr (future_map()) est le choix moderne : c’est un remplacement direct de purrr::map() avec des retours à type stable, et un seul plan() bascule entre le séquentiel, plusieurs sessions locales et des clusters distants sans toucher à votre boucle. foreach (avec doParallel) est plus ancien et partout dans le code existant — pas besoin de le réécrire. Pour du code neuf, furrr est généralement plus facile à appréhender.

Mobilisez DuckDB ou Arrow quand les données sont trop grandes pour tenir confortablement en mémoire, ou quand vous filtrez et agrégez à répétition de gros fichiers Parquet/CSV. Ils lisent depuis le disque et poussent le calcul dans un moteur C++, si bien que vous récupérez un petit résultat sans jamais charger le jeu de données complet dans R. Si les données tiennent déjà en RAM et que le base-R vectorisé ou dplyr est assez rapide, vous n’en avez pas besoin.

Oui — vectorisez d’abord. Le parallélisme répartit le travail sur plusieurs cœurs, mais chaque cœur exécute toujours votre code R, donc une boucle interprétée lente reste lente, juste en parallèle (et vous payez en plus le coût des workers). Vectoriser rend souvent le travail assez rapide pour que vous n’ayez jamais besoin de cœurs. Ne parallélisez que ce qui est déjà efficace et réellement limité par le CPU.

Testez vos connaissances

La fonction ci-dessous calcule un « drapeau » courant sans z-score : pour chaque valeur elle enregistre "high" si la valeur dépasse la moyenne, sinon "low". Elle fait croître le résultat avec c() dans une boucle — le piège quadratique. Réécrivez-la avec un seul appel vectorisé, puis mesurez les deux sur x pour voir la différence.

set.seed(1)
x <- rnorm(1e5)

flag_loop <- function(x) {
  m <- mean(x)
  out <- c()
  for (i in seq_along(x)) out <- c(out, if (x[i] > m) "high" else "low")
  out
}

ifelse() est vectorisée : ifelse(condition, yes, no) évalue le vecteur entier d’un coup. La condition ici est x > mean(x).

set.seed(1)
x <- rnorm(1e5)

flag_vec <- function(x) ifelse(x > mean(x), "high", "low")

# same answer?
identical(flag_loop(x), flag_vec(x))   # TRUE

# how much faster?
bench::mark(
  loop = flag_loop(x),
  vec  = flag_vec(x),
  min_iterations = 3
)[, c("expression", "median", "mem_alloc")]

flag_vec() s’exécute en une petite fraction du temps et n’alloue presque rien, parce que ifelse() fait le vecteur entier en une seule passe compilée au lieu de copier un résultat en croissance 100 000 fois.

Vous avez un fichier Parquet de ventes de 50 Go — bien plus grand que les 16 Go de RAM de votre portable — et il vous faut un chiffre d’affaires total par région. Quelle approche convient ?

A. read.csv() le fichier, puis aggregate() par région B. open_dataset() / read_parquet() de DuckDB, grouper et résumer, puis collect() C. future_map() sur quatre cœurs

Afficher la réponse

B. Le problème est la mémoire, pas le CPU : le fichier ne tient pas en RAM, donc l’option A échoue à la lecture et l’option C devrait d’abord charger les données qu’elle ne peut pas tenir. Un moteur hors-mémoire (Arrow ou DuckDB) ne lit que les deux colonnes dont il a besoin directement depuis le disque, fait le regroupement dans son moteur C++, et renvoie un résumé de quatre lignes — sans jamais charger les 50 Go dans R.

Conclusion

Passer R à l’échelle est une décision, pas une astuce. Quand le code est lent, demandez pourquoi : une boucle interprétée faisant un travail sur vecteur entier veut être vectorisée ; des données qui ne tiennent pas en RAM veulent un moteur hors-mémoire comme Arrow ou DuckDB ; du calcul lourd et indépendant veut être parallélisé avec future et furrr. Parcourez l’arc dans cet ordre, mesurez chaque changement, et vous corrigerez le vrai goulot d’étranglement plutôt que celui imaginé — le jugement qui reste précieux quelle que soit la qualité du copilote qui écrit votre premier jet.

Leçons connexes

Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Les bases de R — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN
🟢 Avec un agent IA

Une boucle for qui rampe sur de vraies données ? Collez-la et demandez à Prova « pourquoi est-ce lent, et devrais-je vectoriser, passer hors-mémoire ou paralléliser ? » — elle diagnostique le goulot d’étranglement et réécrit le code, et comme le runtime est juste là, vous mesurez l’avant et l’après sur vos données et voyez quelle solution l’a vraiment emporté. Demander à Prova →

Note

Cette leçon est reproductible : chaque résultat de cette page a été produit par le code affiché. Copiez n’importe quel bloc et exécutez-le localement pour reproduire ces mesures — les chiffres exacts dépendent de votre machine, mais les écarts relatifs se maintiennent. The runtime is the judge.

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Passer R à l’échelle : vectoriser, hors-mémoire et
    parallélisation},
  date = {2026-07-19},
  url = {https://www.datanovia.com/learn/programming/r-foundations/scaling-r},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Passer R à l’échelle : vectoriser, hors-mémoire et parallélisation.” 2026. July 19. https://www.datanovia.com/learn/programming/r-foundations/scaling-r.