0Pricing
DevOps Bootcamp · Leçon

Profiler les scripts et éviter les sous-interpréteurs inutiles

Mesurez la durée d’exécution des scripts et remplacez les enchaînements gourmands en processus, comme cat suivi de grep, par des alternatives intégrées.

Profiler les scripts et éviter les sous-interpréteurs inutiles est une leçon DevOps Bootcamp gratuite sur CoddyKit. Ceci est la leçon 1 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DevOps Bootcamp, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DevOps Bootcamp comprend 4 leçons au total.

Pourquoi les performances des scripts sont importantes

Les scripts Bash qui s’exécutent lentement gaspillent du temps d’intégration continue, bloquent les tâches cron et frustrent les utilisateurs. La plupart des ralentissements ne proviennent pas d’une logique complexe, mais de créations inutiles de processus : chaque commande externe que vous appelez lance un nouveau processus enfant.

Dans cette leçon, vous apprendrez à :

  • Mesurer où le temps est réellement passé avec time et bash -x
  • Repérer les schémas inefficaces qui créent beaucoup de processus, comme l’utilisation inutile de cat
  • Remplacer les commandes externes par des commandes intégrées du shell, plus rapides
  • Utiliser les sous-shells intentionnellement et les éviter lorsqu’ils n’apportent aucune valeur

Le but est d’écrire des scripts qui effectuent le même travail avec moins de processus enfants et en moins de temps réel.

Chronométrer un script avec la commande intégrée time

L’outil de profilage le plus simple est la commande intégrée du shell time. Placez-la devant n’importe quelle commande ou chaîne de commandes pour obtenir trois mesures :

  • real — temps réel écoulé (le temps pendant lequel vous attendez effectivement)
  • user — temps CPU consacré au code exécuté dans l’espace utilisateur
  • sys — temps CPU consacré au noyau (appels système, entrées-sorties)

Un écart important entre real et user+sys signifie généralement que le script attend des entrées-sorties ou lance de nombreux processus enfants. Exécutez d’abord time autour du script entier pour confirmer l’existence d’un problème avant d’optimiser quoi que ce soit.

#!/usr/bin/env bash
# Time a whole script block
time {
  for i in $(seq 1 1000); do
    echo "line $i"
  done | grep -c "5"
}
# Output example:
# 271
# real  0m0.045s
# user  0m0.038s
# sys   0m0.012s

Suivre l’exécution avec bash -x et PS4

bash -x affiche chaque commande avant son exécution : c’est le suivi de l’exécution. Vous voyez ainsi quelles lignes sont exécutées le plus souvent et si des programmes externes sont appelés plus souvent que prévu.

Par défaut, chaque ligne suivie est précédée de +. Vous pouvez enrichir ce préfixe avec PS4 pour y inclure des horodatages, transformant ainsi le suivi en outil de profilage léger :

  • PS4 est développé avant chaque commande suivie
  • L’inclusion de $EPOCHREALTIME (bash 5+) ou de $(date +%s%N) fournit une résolution à la nanoseconde
  • Redirigez la sortie d’erreur vers un fichier, puis traitez-le pour trouver les sections lentes
#!/usr/bin/env bash
# Run with:  bash -x ./myscript.sh  2>trace.log
# Or embed tracing inside the script:
export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE}:${LINENO}: '
set -x

slow_function() {
  local result
  result=$(cat /etc/hostname)   # fork — slow
  echo "host: $result"
}

slow_function
set +x

# trace.log now contains timestamps so you can diff
# adjacent lines to find which step took longest.

Qu’est-ce qu’un sous-shell inutile ?

Un sous-shell est une copie enfant du processus du shell actuel. Il est créé par :

  • Substitution de commande : $(command)
  • Regroupement entre parenthèses : ( commands )
  • Transmission par tube vers une construction du shell : cmd | while read ...

Les sous-shells sont nécessaires lorsque vous avez réellement besoin d’isolation ou d’un pipeline. Ils deviennent inutiles lorsque vous les utilisez uniquement pour appeler un programme externe que le shell pourrait gérer lui-même, ou lorsque vous encapsulez une commande intégrée dans une couche supplémentaire de création de processus sans raison.

Chaque création d’un sous-shell coûte environ 1 à 5 ms sur un système Linux moderne. Dans une boucle exécutée 10 000 fois, 1 000 sous-shells inutiles ajoutent 1 à 5 secondes de surcharge pure.

Le schéma inefficace classique : l’utilisation inutile de cat

cat file | grep pattern est le schéma inefficace qui crée le plus de processus et qui est le plus connu. Il lance deux processus (cat et grep) reliés par un tube, alors que grep seul peut lire directement le fichier.

La correction est simple : transmettez directement le nom du fichier à la commande qui sait gérer les fichiers. On parle de redirection de l’entrée lorsque l’outil n’accepte pas les noms de fichiers, ou il suffit d’omettre cat lorsqu’il les accepte.

  • Lent : cat file | grep pattern — 2 processus, 1 tube
  • Rapide : grep pattern file — 1 processus, aucun tube
  • Également rapide : grep pattern < file — 1 processus, redirection de l’entrée standard (sans tampon de tube)
#!/usr/bin/env bash
# Create a sample file
seq 1 10000 > /tmp/numbers.txt

# --- Slow: useless cat ---
time cat /tmp/numbers.txt | grep -c "^5"

# --- Fast: grep reads the file directly ---
time grep -c "^5" /tmp/numbers.txt

# Both print the same count; the second is measurably faster
# because it skips the cat process and the inter-process pipe.

Remplacer les commandes externes par des commandes intégrées du shell

De nombreuses transformations écrites sur une seule ligne ont un équivalent intégré qui évite entièrement de créer un processus. Comparez ces substitutions courantes :

  • echo ${#var} au lieu de echo "$var" | wc -c — longueur de chaîne
  • ${var^^} et ${var,,} au lieu de echo "$var" | tr 'a-z' 'A-Z' — conversion de la casse (bash 4+)
  • ${var//search/replace} au lieu de echo "$var" | sed 's/search/replace/' — substitution simple
  • [[ "$var" =~ pattern ]] au lieu de echo "$var" | grep -q pattern — correspondance avec une expression régulière
  • read -r line < file au lieu de line=$(head -n1 file) — lecture de la première ligne

Aucune de ces commandes intégrées ne crée de processus enfant. Le gain est faible à chaque appel, mais il devient considérable dans les boucles.

#!/usr/bin/env bash
sentence="hello world from bash"

# --- Fork-heavy ---
upper_slow=$(echo "$sentence" | tr 'a-z' 'A-Z')
length_slow=$(echo "$sentence" | wc -c)

# --- Builtin equivalents (zero extra processes) ---
upper_fast=${sentence^^}
length_fast=${#sentence}

echo "Slow upper : $upper_slow"
echo "Fast upper : $upper_fast"
echo "Slow length: $length_slow"
echo "Fast length: $length_fast"

Éviter les sous-shells dans les boucles

Une substitution de commande dans une boucle multiplie le coût de création d’un processus par le nombre d’itérations. Une boucle exécutée 500 fois avec un appel $(date) lance 500 processus enfants uniquement pour produire les horodatages.

Stratégies pour réduire la surcharge des boucles :

  • Déplacez les commandes invariantes en dehors de la boucle (calculez une fois, réutilisez)
  • Préférez le développement arithmétique $(( expr )) — il est intégré au shell et ne crée pas de processus
  • Utilisez printf au lieu d’appeler date lorsque seul le formatage est nécessaire
  • Regroupez les appels externes : collectez d’abord les données, puis traitez-les une seule fois en dehors de la boucle
#!/usr/bin/env bash
# Demonstrate: compute-once vs fork-per-iteration

# Bad: $(date) forks 1000 times
time (
  for i in $(seq 1 1000); do
    ts=$(date +%s)   # fork each iteration
    echo "$i $ts" > /dev/null
  done
)

# Good: capture once, reuse
time (
  ts=$(date +%s)   # fork exactly once
  for i in $(seq 1 1000); do
    echo "$i $ts" > /dev/null
  done
)

Sous-shells des tubes et piège de la portée des variables

Dans bash (contrairement à ksh/zsh), chaque commande d’un pipeline s’exécute dans son propre sous-shell. Cela signifie que les variables définies dans un tube sont perdues une fois le tube terminé.

Il s’agit à la fois d’un bogue de correction et d’un problème de performances : vous pouvez transmettre des données à while read en pensant les collecter, puis constater que la variable est vide ensuite.

Deux solutions :

  • Utilisez la substitution de processus while read line; do ...; done < <(command) — la boucle while s’exécute dans le shell actuel, et non dans un sous-shell
  • Utilisez l’option lastpipe (shopt -s lastpipe) — elle fait exécuter le dernier segment du pipeline dans le shell actuel (bash 4.2+)
#!/usr/bin/env bash
count=0

# --- Bug: count is always 0 after pipe (subshell) ---
seq 1 5 | while read -r n; do
  (( count++ ))
done
echo "After pipe   : count=$count"   # prints 0

# --- Fix 1: process substitution (no subshell for while) ---
count=0
while read -r n; do
  (( count++ ))
done < <(seq 1 5)
echo "Process sub  : count=$count"   # prints 5

# --- Fix 2: lastpipe option ---
shopt -s lastpipe
count=0
seq 1 5 | while read -r n; do
  (( count++ ))
done
echo "lastpipe     : count=$count"   # prints 5

Mesurer le coût des sous-shells avec un microbenchmark

Il est facile de démontrer la surcharge des sous-shells avec un petit test de performance. Comparez une opération arithmétique effectuée via $(( )) (commande intégrée) à la même opération transmise à expr (processus externe) par un tube.

Sur un ordinateur Linux courant, les résultats montrent que 10 000 appels à expr prennent environ 5 secondes, tandis que le même nombre d’appels à $(( )) prend moins de 0,1 seconde — une différence d’un facteur 50 pour une sortie identique.

Ce schéma de test est également utile lorsque vous souhaitez mesurer une optimisation : exécutez les deux versions N fois dans une boucle et comparez-les avec time.

#!/usr/bin/env bash
N=500

# External command (fork per call)
time (
  x=0
  for ((i=0; i<N; i++)); do
    x=$(expr $x + 1)   # forks expr each time
  done
  echo "expr result: $x"
)

# Arithmetic builtin (no fork)
time (
  x=0
  for ((i=0; i<N; i++)); do
    (( x++ ))          # pure builtin
  done
  echo "builtin result: $x"
)

Utiliser les chaînes here-string pour éviter les tubes avec echo

Un schéma courant consiste à utiliser echo "$var" | command pour transmettre une variable à l’entrée standard. Cela crée deux processus (echo et command) ainsi qu’un tube. Une chaîne here-string (<<<) produit le même résultat avec un seul processus : la commande externe lit depuis un tampon temporaire géré par le noyau.

  • grep pattern <<< "$var" — un processus, aucun tube
  • read -r field1 field2 <<< "$line" — séparation d’une variable sans outil externe
  • wc -w <<< "$sentence" — comptage des mots d’une variable

Les chaînes here-string sont particulièrement utiles dans les boucles serrées, où chaque création de processus compte.

#!/usr/bin/env bash
data="The quick brown fox"

# --- Fork-heavy: echo spawns a child ---
word_count_slow=$(echo "$data" | wc -w)
echo "Slow word count: $word_count_slow"

# --- Fast: here-string, only wc spawns ---
word_count_fast=$(wc -w <<< "$data")
echo "Fast word count: $word_count_fast"

# --- Even better: use parameter expansion (zero forks) ---
# Split into array, count elements
read -ra words <<< "$data"
echo "Zero-fork count: ${#words[@]}"

Refactorisation pratique : avant et après

Examinons un script réaliste qui traite un fichier journal et appliquons tout ce qui a été appris. La version originale enchaîne cat, grep, awk et tr avec des tubes. La version refactorisée réduit le nombre de processus de 8 à 2.

Principales modifications :

  • Suppression de cat — grep lit directement le fichier
  • Remplacement de tr '[:lower:]' '[:upper:]' par ${var^^}
  • Remplacement de echo "$line" | grep -q par [[ $line =~ ]]
  • Utilisation de read -r avec la substitution de processus au lieu d’une boucle while alimentée par un tube

Après la refactorisation, exécutez de nouveau time ./script.sh pour confirmer l’amélioration. Mesurez toujours : ne faites pas de suppositions.

#!/usr/bin/env bash
# Create a sample log
printf 'ERROR: disk full\nINFO: started\nERROR: timeout\nINFO: done\n' \
  > /tmp/sample.log

# === BEFORE (fork-heavy) ===
time (
  cat /tmp/sample.log \
    | grep 'ERROR' \
    | while read -r line; do
        label=$(echo "$line" | tr '[:lower:]' '[:upper:]')
        echo "[ALERT] $label"
      done
)

# === AFTER (builtin-first) ===
time (
  while IFS= read -r line; do
    echo "[ALERT] ${line^^}"
  done < <(grep 'ERROR' /tmp/sample.log)
)

Vérification des connaissances : portée des sous-interpréteurs

Vérifiez votre compréhension des sous-interpréteurs des pipelines et de la manière d’éviter de perdre les modifications de variables effectuées à l’intérieur d’un pipeline.

Récapitulatif de la leçon : profiler d’abord, bifurquer moins

Dans cette leçon, vous avez appris à identifier et à éliminer les sources les plus courantes de création inutile de processus dans les scripts bash.

Points essentiels à retenir :

  • Utilisez time et bash -x enrichi avec PS4 pour mesurer avant d’optimiser
  • Useless cat est l’anti-modèle le plus répandu : transmettez directement les noms de fichiers aux commandes qui les acceptent
  • Remplacez echo "$var" | command par une chaîne here-string (command <<< "$var") ou une commande intégrée
  • Les expansions de paramètres (${var^^}, ${var//s/r}, ${#var}) remplacent de nombreux appels à tr, sed et wc
  • Les sous-interpréteurs des pipelines absorbent les modifications de variables : utilisez la substitution de processus ou shopt -s lastpipe
  • Déplacez les appels de commandes invariants en dehors des boucles ; préférez l’arithmétique $(( )) à expr

La règle générale : mesurez d’abord, remplacez autant que possible les commandes externes par des commandes intégrées, puis vérifiez l’amélioration avec une deuxième mesure.

Questions Fréquemment Posées

La leçon « Profiler les scripts et éviter les sous-interpréteurs inutiles » est-elle gratuite ?

Oui — le texte complet de « Profiler les scripts et éviter les sous-interpréteurs inutiles » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DevOps Bootcamp, passe à CoddyKit PRO. Le cours DevOps Bootcamp comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Profiler les scripts et éviter les sous-interpréteurs inutiles » ?

Mesurez la durée d’exécution des scripts et remplacez les enchaînements gourmands en processus, comme cat suivi de grep, par des alternatives intégrées. Tu pratiques DevOps Bootcamp avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer DevOps Bootcamp ?

Aucune expérience préalable n'est requise. DevOps Bootcamp sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 1 sur 4.

Combien de temps prend la leçon « Profiler les scripts et éviter les sous-interpréteurs inutiles » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon DevOps Bootcamp ?

Oui. Chaque leçon DevOps Bootcamp inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Profiler les scripts et éviter les sous-interpréteurs inutiles
  2. Parallélisme avec xargs -P et tâches en arrière-plan
  3. Orchestrer des charges de travail avec GNU parallel
  4. Pipelines de flux et tubes nommés pour le débit
← Retour à DevOps Bootcamp