Où regarder quand un script PowerShell est lent — les points clés sur les tableaux, le pipeline et le rapprochement de données

· · PowerShell, Windows, Amélioration des performances, Automatisation, Script, Amélioration opérationnelle, Optimisation, Traitement des données

« Le script d’agrégation qui se terminait en quelques minutes au début met désormais trois heures, maintenant que les données ont augmenté. » — C’est un témoignage fréquent sur le terrain, là où l’automatisation avec PowerShell s’est installée durablement. Et dans la plupart des cas, la cause de la lenteur ne tient pas à la vitesse d’exécution de PowerShell lui-même, mais à la façon dont le code est écrit.

PowerShell étant un langage qui privilégie la facilité d’écriture, il existe plusieurs façons d’écrire qui, tout en semblant naturelles, dégradent la complexité algorithmique. L’exemple emblématique est $array += $item. Son apparence est naturelle, mais en interne, une copie de la totalité du tableau s’exécute à chaque fois, et le ralentissement devient brutal à mesure que le nombre d’éléments augmente. Connaître ou non ces pièges classiques détermine si le même traitement prend plusieurs heures ou quelques dizaines de secondes.

Cet article organise, par ordre d’impact décroissant, les points à suspecter en premier lorsque vous constatez un script lent. Il aborde aussi la bonne façon de mesurer, pour éviter de réécrire le code sur de simples suppositions.

Cet article couvre à la fois Windows PowerShell 5.1 et PowerShell 7. Les points où le comportement diffère entre les deux (comme le codage de caractères par défaut de Get-Content) sont signalés au fil du texte. Les exemples de code de l’article ont été exécutés et vérifiés avec PowerShell 7.6.

1. La conclusion, d’abord

  • N’utilisez pas $array += $item. Les tableaux PowerShell sont de longueur fixe, et += crée à chaque fois un nouveau tableau en copiant tous les éléments. Le ralentissement est proportionnel au carré du nombre d’éléments.1
  • Utilisez à la place List[T], ou recevez directement dans une variable la sortie de toute la boucle. Cette seconde option est à la fois idiomatique en PowerShell et rapide.
  • Pour le rapprochement et la recherche, la mise en table de hachage est ce qui a le plus d’effet. La recherche linéaire en double boucle (O(n×m)) devient un accès par clé (à peu près O(n+m)).2
  • Le pipeline est pratique, mais il a un coût par objet. Dans une boucle interne portant sur un grand nombre d’éléments, l’instruction foreach est souvent plus rapide, et cela vaut la peine d’être mesuré.3
  • Choisissez la méthode de lecture de fichier selon l’objectif. Get-Content -Raw pour une lecture globale, -ReadCount pour une lecture par lots. La transformation en objet ligne par ligne peut peser dans certains cas.4
  • Utilisez -Filter avec Get-ChildItem. Le filtrage s’effectue côté fournisseur, ce qui est plus efficace que de récupérer puis d’éliminer avec Where-Object, comme le précise explicitement la documentation officielle.5
  • Pour concaténer des chaînes, utilisez -join ou StringBuilder. $s += "..." ralentit pour la même raison que les tableaux.6
  • Réservez Format-* à la toute fin, pour l’affichage à l’écran. L’insérer en cours de route casse le traitement en aval et ajoute un coût de formatage inutile.7
  • Mesurez avant de corriger. Measure-Command rejette la sortie et n’inclut donc pas le coût d’affichage. N’utilisez pas la valeur de la première exécution.8
  • La parallélisation vient en dernier. Envisagez-la seulement après avoir corrigé l’algorithme (voir « Le traitement parallèle en PowerShell »).

2. Mesurer d’abord — la bonne utilisation de Measure-Command

Optimiser sur la base de suppositions revient le plus souvent à corriger un endroit qui n’a aucun effet. La première chose à faire est de mesurer.8

Une précision d’abord. Invoke-KsAggregate, ConvertTo-KsRecord et Join-KsRecord, qui apparaissent dans les exemples de code de cet article, sont des noms de fonctions fictifs, à remplacer mentalement par votre propre traitement. Ce ne sont pas des applets de commande standard de PowerShell, donc si vous les collez telles quelles, l’exécution s’arrêtera sur une erreur « terme non reconnu ». Lisez-les en les remplaçant par les noms de fonctions et le traitement de votre propre script.

# On écarte la première mesure, qui inclut le chargement des modules et la compilation JIT
$null = Measure-Command { Invoke-KsAggregate }

# On mesure plusieurs fois à partir de la deuxième exécution, pour observer aussi la dispersion
1..3 | ForEach-Object {
    (Measure-Command { Invoke-KsAggregate }).TotalSeconds
}

Deux points de vigilance concernant Measure-Command.

  • La sortie du bloc de script est rejetée. Le coût de mise en forme et de rendu à l’écran n’est pas inclus. Si la lenteur ressentie en pratique est différente, le coupable peut se trouver du côté de l’affichage
  • Comparez dans les mêmes conditions. Selon que le cache de fichiers est déjà « chaud » ou non, la valeur mesurée d’un traitement incluant des entrées/sorties peut varier considérablement

Pour découper précisément où, dans le traitement, se situe la lenteur, utilisez Stopwatch.

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$rows = Import-Csv $csvPath
Write-Verbose "Lecture : $($sw.ElapsedMilliseconds) ms"; $sw.Restart()

$index = $master | Group-Object Code -AsHashTable -AsString
Write-Verbose "Création de l'index : $($sw.ElapsedMilliseconds) ms"; $sw.Restart()

$result = foreach ($r in $rows) { Join-KsRecord $r $index }
Write-Verbose "Rapprochement : $($sw.ElapsedMilliseconds) ms"; $sw.Stop()

En sortant ainsi le temps de chaque section, vous découvrez immédiatement des faits comme « en réalité, la lecture représentait 80 % du temps ». Si Write-Verbose est utilisé ici, c’est pour ne rien afficher en exécution normale et ne le voir que lors d’une investigation (voir « Abandonner Write-Host — flux de sortie et conception des journaux en PowerShell »).

3. Le coupable principal — l’opérateur += sur un tableau

Les tableaux PowerShell sont de longueur fixe. Il est impossible d’y ajouter un élément, et += se développe en un traitement « créer un nouveau tableau, copier tous les éléments, puis ajouter le nouvel élément à la fin ».1 Autrement dit, une boucle qui ajoute n éléments effectue au total un nombre de copies proportionnel au carré de n.

L’ampleur de cette croissance peut se calculer directement à partir de l’écriture du code. Le nombre total de copies d’éléments qui se produit lors de l’ajout de n éléments est n(n-1)/2.

Nombre d’éléments ajoutés Nombre total de copies d’éléments avec += Nombre d’appels à List[T].Add
1 000 éléments environ 500 000 1 000
10 000 éléments environ 50 000 000 10 000
100 000 éléments environ 5 000 000 000 100 000

Il ne s’agit pas d’une mesure réelle de temps d’exécution, mais d’un nombre d’opérations déterminé de façon unique par l’écriture du code. Le temps réel par élément varie selon l’environnement, donc mesurez les secondes propres à votre environnement avec le banc d’essai du zip distribué. Ce qui reste toutefois identique quel que soit l’environnement, c’est cette croissance : multiplier le nombre d’éléments par 10 multiplie le nombre d’opérations par 100.

# [À ÉVITER] devient brutalement lent quand le nombre d'éléments augmente
$result = @()
foreach ($row in $rows) {
    $result += ConvertTo-KsRecord $row     # tout le tableau est copié à chaque fois
}

# [BON-1] Utiliser List[T] (ajout d'élément en temps constant)
$result = [System.Collections.Generic.List[object]]::new()
foreach ($row in $rows) {
    $result.Add((ConvertTo-KsRecord $row))
}

# [BON-2] Recevoir directement dans une variable la sortie de toute la boucle (idiomatique en PowerShell, et rapide)
$result = foreach ($row in $rows) {
    ConvertTo-KsRecord $row                # la sortie est agrégée directement
}

L’écriture [BON-2] est, une fois qu’on s’y habitue, la forme la plus naturelle en PowerShell. La sortie de foreach, ForEach-Object, if, etc. peut être directement agrégée dans une variable. Cette propriété s’acquiert en même temps que la compréhension des « flux de sortie ».

Le même raisonnement s’applique aux chaînes de caractères. Une chaîne étant immuable, $s += "line" crée une nouvelle chaîne à chaque fois.

# [À ÉVITER]
$text = ''
foreach ($line in $lines) { $text += "$line`r`n" }

# [BON-1] -join (le plus concis)
$text = $lines -join "`r`n"

# [BON-2] StringBuilder (pour une construction complexe impliquant des branchements conditionnels)
$sb = [System.Text.StringBuilder]::new()
foreach ($line in $lines) { [void]$sb.AppendLine($line) }
$text = $sb.ToString()

4. Abandonner la double boucle — le rapprochement de données avec une table de hachage

Le point suivant qui a le plus d’effet est le rapprochement (matching) de données. Écrire naïvement le traitement « pour chaque ligne des données de commande, aller chercher le nom du produit dans la table maîtresse » donne ceci.

# [À ÉVITER] 10 000 commandes × 10 000 lignes maîtresses = 100 millions de comparaisons
foreach ($order in $orders) {
    $item = $master | Where-Object { $_.Code -eq $order.Code }   # parcourt l'intégralité de la table à chaque fois
    $order | Add-Member NoteProperty ItemName $item.Name
}

En le remplaçant par une table de hachage, le nombre de comparaisons chute drastiquement.2

# [BON] Construire l'index une seule fois, puis accéder par clé
$index = $master | Group-Object -Property Code -AsHashTable -AsString

$result = foreach ($order in $orders) {
    $hit = $index[$order.Code]        # l'accès par clé ne dépend pas du nombre d'éléments
    [pscustomobject]@{
        Code     = $order.Code
        Qty      = $order.Qty
        ItemName = if ($hit) { $hit[0].Name } else { $null }   # permet aussi de détecter les éléments non enregistrés
    }
}

Regardons ici aussi le nombre d’opérations. Le nombre de comparaisons de la double boucle est le nombre de commandes multiplié par le nombre de lignes maîtresses, tandis que la table de hachage, avec « construire l’index une fois » plus « chercher élément par élément », est proportionnelle au nombre total d’éléments.

Commandes × lignes maîtresses Comparaisons de la double boucle Opérations de la table de hachage
1 000 × 1 000 1 000 000 environ 2 000
10 000 × 10 000 100 000 000 environ 20 000
100 000 × 100 000 10 000 000 000 environ 200 000

Quand le nombre d’éléments est multiplié par 10, la double boucle l’est par 100 et la table de hachage par 10 seulement. Plus le volume est important, plus l’écart se creuse : il vaut donc la peine de corriger en priorité les traitements dont le volume de données est appelé à augmenter.

Group-Object -AsHashTable regroupe en tableau les éléments partageant la même clé, si bien que la valeur est un tableau (d’où $hit[0] ci-dessus). Si vous savez que les clés sont uniques, vous pouvez aussi construire vous-même la table de hachage.

$index = @{}
foreach ($m in $master) { $index[$m.Code] = $m }    # suppose des clés uniques

Lorsqu’il s’agit seulement de tester à répétition « est-ce que cela y figure ? », HashSet est efficace. -contains et -in sur un tableau font une recherche linéaire, ce qui pèse dès que le nombre de vérifications devient important.

$known = [System.Collections.Generic.HashSet[string]]::new(
    [string[]]$master.Code, [System.StringComparer]::OrdinalIgnoreCase)

$unknown = $orders | Where-Object { -not $known.Contains($_.Code) }

Les schémas pratiques de rapprochement entre fichiers CSV sont également traités dans « Automatiser le traitement métier Excel et CSV avec PowerShell ».

5. Le pipeline et l’instruction foreach

Le pipeline est une fonctionnalité centrale de PowerShell, mais il a un coût de traitement pour faire circuler chaque objet entre les applets de commande. Dans une boucle interne portant sur plusieurs centaines de milliers d’éléments, l’instruction foreach est souvent plus rapide.3

# Pipeline : lisible, et comme le traitement est séquentiel, aucun résultat intermédiaire ne s'accumule
$rows | Where-Object { $_.Status -eq 'OK' } | ForEach-Object { $_.Amount } |
    Measure-Object -Sum

# Instruction foreach : surcoût par élément faible, plus rapide sur un grand nombre d'éléments
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }

Un mot sur la mémoire, car ce point prête souvent à confusion. L’instruction foreach évalue d’abord l’expression entre parenthèses, puis entre dans la boucle : si vous lui passez le résultat de l’exécution d’une commande, comme dans foreach ($line in (Get-Content $path)), la totalité des éléments est chargée en mémoire à ce moment-là.3 En revanche, si vous passez un IEnumerable à énumération différée, les éléments sont énumérés un par un. Vous pouvez donc traiter même un fichier volumineux sans utiliser de mémoire, tout en gardant l’instruction foreach, en écrivant comme suit.

# Traiter avec foreach sans charger tous les éléments en mémoire (énumération différée)
foreach ($line in [System.IO.File]::ReadLines($path)) {
    if ($line.StartsWith('ERROR')) { $errors++ }
}

Attention toutefois : le codage de caractères par défaut diffère. ReadLines($path) avec un seul argument lit en UTF-8 (en privilégiant le BOM s’il est présent). En revanche, sous Windows PowerShell 5.1, Get-Content lit, en l’absence de BOM, avec la page de code ANSI courante (Shift_JIS dans un environnement japonais). Autrement dit, remplacer telle quelle une lecture d’un journal en Shift_JIS provoque des caractères corrompus pour le japonais. Lors du remplacement, utilisez la surcharge qui permet de spécifier explicitement l’encodage.

# Pour lire un journal en Shift_JIS (CP932)
# À partir de PowerShell 6.2, le fournisseur de pages de code est déjà enregistré,
# donc GetEncoding(932) peut être appelé directement (de même sous Windows PowerShell 5.1)
$enc = [System.Text.Encoding]::GetEncoding(932)
foreach ($line in [System.IO.File]::ReadLines($path, $enc)) {
    if ($line.StartsWith('ERROR')) { $errors++ }
}

Notez par ailleurs que sous PowerShell 7, Get-Content a pour défaut l’UTF-8 (sans BOM), donc tant que vous manipulez des fichiers UTF-8 sous PowerShell 7, la surcharge à un seul argument donne le même résultat. Avant tout remplacement, vérifiez systématiquement le codage de caractères du fichier cible et la version de l’environnement d’exécution.

Voici un repère de décision.

Situation Choix
Peu d’éléments, lisibilité prioritaire Pipeline
Entrée volumineuse issue du résultat d’une commande (Get-Content, etc.) Pipeline, ou [IO.File]::ReadLines + instruction foreach
Parcourir plusieurs centaines de milliers d’éléments d’un tableau déjà en mémoire Instruction foreach
Filtrage simple sur un tableau Méthodes .Where({...}) / .ForEach({...})1

.Where() et .ForEach() sont des méthodes de tableau ajoutées dans PowerShell 4.0 ; comme elles ne passent pas par le pipeline, elles sont plus légères.1 Cela suppose cependant que la cible soit déjà une collection en mémoire.

6. Les entrées/sorties de fichiers et de répertoires

Pour la lecture, choisissez selon l’objectif. Get-Content génère par défaut un objet par ligne, un coût qui devient dominant sur les fichiers volumineux.4

Get-Content $path                      # Ligne par ligne (génère autant d'objets que de lignes)
Get-Content $path -Raw                 # Lit l'ensemble en une seule fois, comme une seule chaîne
Get-Content $path -ReadCount 1000      # Transmet par lots de 1000 lignes sous forme de tableau (réduit la génération d'objets)
[System.IO.File]::ReadLines($path)     # .NET. Le plus léger, par énumération séquentielle (par défaut en UTF-8)

Pour le parcours de répertoires, utilisez -Filter. La documentation officielle précise explicitement que « le filtre est appliqué par le fournisseur au moment de la récupération des objets, ce qui le rend plus efficace que les autres paramètres ».5

# [À ÉVITER] récupère tout puis élimine
Get-ChildItem -Path $root -Recurse | Where-Object { $_.Extension -eq '.log' }

# [BON] filtre côté fournisseur
Get-ChildItem -Path $root -Recurse -File -Filter '*.log'

# À l'échelle de centaines de milliers de fichiers, si c'est encore lent, envisagez l'énumération .NET
[System.IO.Directory]::EnumerateFiles($root, '*.log', 'AllDirectories')

Pour l’écriture, l’ajout répété à chaque itération de boucle est un goulot d’étranglement classique. Add-Content ouvre et ferme le fichier à chaque appel.

# [À ÉVITER] 10 000 lignes = 10 000 ouvertures/fermetures
foreach ($r in $result) { Add-Content -Path $out -Value ($r -join ',') }

# [BON-1] Écrire en une seule fois, regroupé
$result | ForEach-Object { $_ -join ',' } | Set-Content -Path $out -Encoding utf8

# [BON-2] Si une écriture séquentielle est nécessaire, garder un StreamWriter ouvert
$writer = [System.IO.StreamWriter]::new($out, $false, [System.Text.UTF8Encoding]::new($false))
try   { foreach ($r in $result) { $writer.WriteLine($r -join ',') } }
finally { $writer.Dispose() }

Pour les opérations sur des fichiers via le réseau, c’est avant tout le nombre d’aller-retours qui domine. Pour les précautions propres aux chemins UNC, voir « Les pièges des partages réseau et des chemins UNC ».

7. Les petits coûts que l’on néglige facilement

Pour vous aider à décider par où commencer, voici un repère de l’ampleur de l’effet. Il ne s’agit pas de mesures réelles, mais d’un ordre de grandeur déterminé par « le coût par occurrence × le nombre d’exécutions ». Lisez-le en le rapportant au volume de votre propre script.

  • La fréquence de mise à jour de Write-Progress. Mettre à jour la progression à chaque itération peut faire dépasser le coût de rendu sur le temps de traitement lui-même. Réduisez la fréquence (par exemple une mise à jour toutes les 100 itérations), ou désactivez-la en exécution sans surveillance avec $ProgressPreference = 'SilentlyContinue'effet estimé : élevé. Le rendu par occurrence est coûteux, et le nombre d’itérations pèse directement. Pour une boucle de plusieurs dizaines de milliers d’itérations ou plus, c’est le premier point à regarder
  • Insérer Format-Table / Format-List en cours de route. Cela convertit en objets de formatage, ce qui casse le traitement en aval et représente un coût inutile. Réservez l’affichage à la toute fin7effet estimé : moyen. Le fait que « le traitement en aval se casse » cause plus de dégâts réels que la vitesse elle-même ; cela vaut la peine d’être corrigé même sans objectif de performance
  • Les traitements invariants à l’intérieur d’une boucle. Sortir simplement de la boucle des éléments comme la génération de chaîne de format pour Get-Date, la compilation d’expressions régulières ou le rechargement de module peut suffire à avoir un effet — effet estimé : moyen à élevé. Cela dépend du coût de l’opération sortie de la boucle ; si quelque chose de lourd, comme un rechargement de module, s’y trouve mêlé, l’effet peut être spectaculaire
  • L’usage répété de Select-Object -Property. Cela génère un nouveau PSCustomObject, ce qui n’est pas négligeable sur un grand nombre d’éléments. Conserver l’objet d’origine jusqu’à l’étape où c’est nécessaire peut être plus rapide — effet estimé : faible à moyen. Imperceptible avec quelques milliers d’éléments, il commence à peser au-delà de quelques dizaines de milliers
  • Les exceptions à répétition. Une conception qui fait passer massivement le traitement par try/catch (par exemple appeler Get-Item à chaque fois sur un fichier inexistant et capturer l’exception) est coûteuse. Préférez un branchement préalable avec Test-Patheffet estimé : élevé. Lever et capturer une exception est bien plus lourd qu’un branchement normal, si bien que cela devient d’autant plus dominant que le produit (nombre d’éléments × taux d’exceptions) est élevé

8. Les règles de l’art en pratique (tableau de décision)

Symptôme Point à suspecter Correction Pour en savoir plus
Devient soudainement lent quand le nombre d’éléments augmente $array += / $string += List[T], réception de la sortie de boucle dans une variable, -join1 Chapitre 3
Le rapprochement de deux jeux de données ne se termine jamais Recherche linéaire en double boucle Table de hachage / Group-Object -AsHashTable2 Chapitre 4
La lecture d’un fichier volumineux est lente Génération d’objets par ligne de Get-Content -Raw / -ReadCount / [File]::ReadLines4 Chapitre 6 (précaution sur le codage de caractères au chapitre 5)
Le parcours de dossiers est lent Filtrage en aval avec Where-Object Get-ChildItem -Filter, EnumerateFiles si nécessaire5 Chapitre 6
L’écriture du fichier de sortie est lente Add-Content dans une boucle Écriture regroupée, ou StreamWriter6 Chapitre 6
Le CPU est inactif mais rien ne se termine Attente réseau ou E/S C’est le moment de paralléliser Article séparé « Traitement parallèle »
L’affichage à l’écran est lent Traitement de formatage, rendu Réserver Format-* à la fin, désactiver $ProgressPreference7 Chapitre 7
Impossible de savoir où est la lenteur Mesures insuffisantes Mesure par section avec Stopwatch, Measure-Command à partir de la deuxième exécution8 Chapitre 2

9. Conclusion

  • Mesurez d’abord. Attention : Measure-Command rejette la sortie et n’inclut pas le coût d’affichage, et la valeur de la première exécution n’est pas utilisable.
  • $array += ralentit proportionnellement au carré du nombre d’éléments. Le remplacer par List[T] ou par la réception de la sortie de boucle dans une variable est la priorité absolue.
  • Pour le rapprochement, la mise en table de hachage est l’amélioration au meilleur rapport coût-bénéfice. Dès que vous repérez une double boucle, demandez-vous si vous pouvez construire un index.
  • Pour les entrées/sorties de fichiers, choisissez la lecture et l’écriture selon l’objectif. La base consiste à filtrer côté fournisseur avec -Filter, à écrire en une fois, et à utiliser StreamWriter au bon moment.
  • Ne pas insérer Format-* en cours de route, réduire la fréquence de mise à jour de la progression, sortir de la boucle les traitements invariants — ces petites accumulations comptent aussi lorsque le volume est important.
  • La parallélisation est le dernier recours. Appliquez-la, après avoir corrigé l’algorithme, aux traitements dominés par des temps d’attente.

Télécharger le code d’exemple

Le code traité dans cet article est distribué sous une forme directement exécutable. Il contient trois types de bancs d’essai et un utilitaire de mesure.

Télécharger le code d’exemple (zip)

Les exemples de cet article ont été réellement exécutés et vérifiés avec PowerShell 7.6 (8 tests Pester). En exécutant Invoke-SampleTests.ps1, inclus dans le zip, vous pouvez reproduire la même vérification chez vous.

# Analyse syntaxique + analyse statique + tests Pester
./Invoke-SampleTests.ps1

Les valeurs de configuration (chemins, noms de serveur, ID de locataire, etc.) sont des exemples. Ne les exécutez pas telles quelles en environnement de production ; adaptez-les à votre propre environnement.

Articles connexes

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge l’accélération des traitements d’agrégation et de rapprochement devenus trop lents, la refonte de la conception des traitements pour résister à l’augmentation du volume de données, ainsi que l’investigation des causes de dégradation des performances.

Références

  1. Microsoft Learn, about_Arrays. Sur le fait que les tableaux PowerShell sont de taille fixe et que l’opérateur += crée un nouveau tableau en copiant les éléments existants avant d’ajouter le nouvel élément ; sur le fait qu’un type de collection (comme List) convient mieux pour des ajouts et suppressions répétés ; et sur les méthodes .Where() et .ForEach(), ajoutées dans PowerShell 4.0.  2 3 4 5

  2. Microsoft Learn, Group-Object. Sur le fait que -AsHashTable permet de retourner le résultat du regroupement sous forme de table de hachage, que -AsString permet de traiter la clé comme une chaîne, et que la valeur de la table de hachage retournée est un tableau des éléments de chaque groupe.  2 3

  3. Microsoft Learn, about_Foreach. Sur le fait que l’instruction foreach évalue l’expression entre parenthèses avant de démarrer l’itération, si bien que passer le résultat d’exécution d’une commande le maintient en mémoire, tandis que l’applet de commande ForEach-Object reçoit son entrée de façon séquentielle depuis le pipeline, ainsi que sur le choix entre les deux. Comme exemple d’énumération différée, la méthode File.ReadLines explique qu’elle permet d’énumérer ligne par ligne sans charger le fichier entier (différence avec ReadAllLines).  2 3

  4. Microsoft Learn, Get-Content. Sur le fait que, par défaut, le contenu est retourné ligne par ligne comme objets séparés par des retours à la ligne, que -Raw permet de lire le fichier entier comme une seule chaîne, et que -ReadCount permet d’envoyer au pipeline des lots d’un nombre de lignes donné.  2 3

  5. Microsoft Learn, Get-ChildItem. Sur le fait que -Filter est appliqué par le fournisseur au moment de la récupération des objets, ce qui le rend plus efficace que les autres paramètres qui filtrent après récupération, et sur sa combinaison avec -File et -Recurse.  2 3

  6. Microsoft Learn, Classe StringBuilder. Sur le fait que String étant immuable, une nouvelle instance est générée à chaque concaténation, alors que StringBuilder construit la chaîne sur un tampon modifiable. Également sur le comportement de la classe StreamWriter, qui écrit séquentiellement en gardant le flux ouvert.  2

  7. Microsoft Learn, Format-Table. Sur le fait que les applets de commande de la famille Format génèrent des objets de formatage destinés à l’affichage, ce qui les rend inadaptées pour être transmises par pipe à une commande suivante, et qu’elles doivent être utilisées en fin de pipeline.  2 3

  8. Microsoft Learn, Measure-Command. Sur le fait qu’elle mesure le temps d’exécution d’un bloc de script ou d’une commande et retourne un TimeSpan, la sortie de l’élément mesuré n’étant elle-même pas incluse dans le résultat. Également sur la mesure par intervalle avec la classe Stopwatch 2 3

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi l'écriture $array += $item est-elle lente ?
Parce que les tableaux PowerShell sont de longueur fixe et ne permettent pas d'ajouter d'élément. L'opérateur += se traduit par le traitement suivant : « créer un nouveau tableau, copier tous les éléments existants, puis ajouter le nouveau à la fin ». Comme une copie complète s'exécute à chaque ajout, ajouter n éléments donne une complexité proportionnelle au carré de n. Avec 100 éléments, cela passe inaperçu, mais au-delà de 10 000 éléments le ralentissement devient perceptible, et à 100 000 éléments ce n'est plus utilisable en pratique. La solution consiste à utiliser System.Collections.Generic.List[T] avec Add, ou à changer d'écriture pour recevoir directement dans une variable la sortie de toute la boucle.
J'ai mesuré avec Measure-Command, mais cela ne correspond pas au temps d'exécution réel.
Measure-Command ne mesure que le temps d'exécution du bloc de script, et sa sortie est rejetée. Dans une exécution réelle s'ajoutent le coût de mise en forme du résultat pour l'affichage (le traitement de formatage) et le temps de rendu vers la console. De plus, la première exécution inclut le temps de chargement des modules et de compilation JIT, si bien que la valeur de la première mesure n'est souvent pas fiable. Respectez ces deux règles : exécutez le même traitement plusieurs fois et regardez les valeurs à partir de la deuxième exécution, et mesurez toujours les éléments à comparer dans les mêmes conditions.
Le rapprochement de deux fichiers CSV ne se termine jamais. Que faut-il corriger ?
Il est très probable que la boucle interne effectue à chaque fois une recherche linéaire avec Where-Object. Si chaque fichier compte 10 000 lignes, le nombre de comparaisons atteint 100 millions. En convertissant l'un des deux en table de hachage (ou avec Group-Object -AsHashTable) pour accéder par clé, le nombre de comparaisons retombe à un ordre de grandeur proportionnel au nombre total de lignes. C'est l'amélioration au meilleur rapport coût-bénéfice, dont l'effet grandit d'autant plus que le volume de données augmente.
On m'a dit qu'appeler directement une API .NET est plus rapide qu'une applet de commande. Faut-il toujours faire ainsi ?
Non, cela dépend du contexte. Les API .NET (par exemple ReadLines ou EnumerateFiles de System.IO.File) sont effectivement rapides, mais vous perdez la commodité des fonctionnalités de fournisseur de PowerShell, des caractères génériques et de la résolution des chemins relatifs, et le code devient aussi moins lisible. Mesurez d'abord, et ne remplacez que les endroits identifiés comme un véritable goulot d'étranglement. Il est plus pragmatique de réserver cela aux cas où l'effet est net, comme le parcours de centaines de milliers de fichiers ou la lecture de fichiers volumineux.
La parallélisation du traitement le rend-elle plus rapide ?
Cela a un effet si le traitement est dominé par des temps d'attente, mais dans l'ordre des choses à faire, cela vient en dernier. Commencez par réduire le travail inutile (limiter le nombre d'éléments récupérés, sortir de la boucle les traitements invariants, transformer le rapprochement en table de hachage). Paralléliser un algorithme resté en O(n²) ne l'accélère que d'un facteur égal au nombre de cœurs. À l'inverse, paralléliser un traitement où chaque élément est léger peut même le ralentir à cause du surcoût.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog