Introduction
PowerShell excelle pour le glue work, l'automatisation système et la mise en forme de données. Pourtant, les performances varient fortement selon la composition des commandes et la structure des pipelines. Quelques changements ciblés suffisent souvent à réduire le temps d'exécution, la mémoire et la latence sans entamer la lisibilité. Ce guide présente des étapes sûres et mesurables à appliquer à vos scripts :
- Inventorier versions et ressources hôtes
- Identifier latence et goulots de débit
- Appliquer des techniques de tuning pratiques
- Vérifier les gains avec des contrôles observables
- Gérer les modes d'échec et revenir en arrière proprement
- Exploiter une checklist reproductible pour le tuning continu
Objectif : créer un pilote restreint, l'améliorer en local (y compris dans Docker ou en développement local), puis généraliser avec confiance. Côté recherche, vous croiserez souvent PowerShell performance, PowerShell tuning, PowerShell optimization, PowerShell latency et PowerShell bottlenecks.
Inventaire versions et environnement
Avant de tuner, capturez l'environnement. De nombreux soucis proviennent de contraintes de runtime ou de ressources.
- Versions et ressources de l'hôte
$PSVersionTable
Get-Host | Select-Object Name, Version
[Environment]::Is64BitProcess
[Environment]::ProcessorCount
Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory
Get-Process -Id $PID | Select-Object Id, ProcessName, WS, PM, CPU
- Modules et chemins (l'autoload peut coûter au démarrage)
$env: PSModulePath -split ';'
Get-Module -ListAvailable | Sort-Object Version -Descending | Select-Object -First 10 Name, Version
- Topologie du script
- Local uniquement vs appels distants (Invoke-Command, REST, base de données)
- Tailles de fichiers/répertoires à parcourir
- Concurrence prévue (monothread vs fan-out)
Cet inventaire fixe la baseline et éclaire s'il faut optimiser le script, l'hôte, ou une dépendance externe.
Triage rapide des goulots
| Symptôme | Cause probable | Vérification |
|---|---|---|
| Lent dès la première ligne | Autoload de modules ou profil | Measure-Command { pwsh -NoProfile -File .\script.ps1 } |
| Lenteur sur gros volumes | Surcharges par objet du pipeline | Comparer foreach vs ForEach-Object avec Measure-Command |
| Lenteur réseau | Latence distante ou DNS | Measure-Command { Invoke-RestMethod ... } et Test-NetConnection |
| Mémoire haute puis pauses GC | Chargement total en mémoire | Streamer avec Get-Content -ReadCount ou File.ReadLines |
Chemin de configuration sûr
Un chemin sûr évite les risques et simplifie le rollback.
- Figer un petit pilote représentatif
- Choisir un script/fonction et un dataset modeste.
- Définir une métrique de succès (ex. -30% de temps).
- Capturer une baseline propre
$sw = [System.Diagnostics.Stopwatch]::StartNew()
& .\pilot.ps1 -InputPath .\sample-input.json -OutPath .\out.json
$sw.Stop(); "Baseline: {0: n2}s" -f $sw.Elapsed.TotalSeconds
- Une seule modification à la fois
- Cibler un hotspot, relancer, consigner les résultats.
- Garder une copie d'origine pour diff et rollback.
- Vérifier la parité fonctionnelle
$expected = Get-Content .\expected.json -Raw
$actual = Get-Content .\out.json -Raw
if ($expected -ne $actual) { throw 'Output mismatch; aborting change.' }
- N'intégrer qu'après des gains reproductibles sur plusieurs runs.
Optimiser le débit : réduire le coût par objet
Le débit est dominé par le nombre d'objets et de transformations. Minimisez le travail par élément et évitez les allocations inutiles.
Préférer foreach (mot-clé) pour les boucles serrées
# Baseline : pipeline
Measure-Command {
1..50000 | ForEach-Object { [math]::Sqrt($_) > $null }
}
# Optimisé : boucle en mémoire
Measure-Command {
foreach ($n in 1..50000) { [math]::Sqrt($n) > $null }
}
Attendu : foreach est plus rapide sur de grandes itérations. Comparez TotalMilliseconds.
Filtrer tôt, projeter tard
# Moins efficace : filtrage après énumération
Get-ChildItem C:\Logs -Recurse | Where-Object { $_.Extension -eq '.log' }
# Plus efficace : filtrage par le provider
Get-ChildItem C:\Logs -Filter *.log -File -Recurse -ErrorAction SilentlyContinue
Vérifiez en mesurant et en comptant les éléments.
Measure-Command { Get-ChildItem C:\Logs -Recurse | Where-Object Extension -eq '.log' | Out-Null }
Measure-Command { Get-ChildItem C:\Logs -Filter *.log -File -Recurse | Out-Null }
Streamer les gros fichiers
# Streaming .NET pour réduire la mémoire
$path = '.\\large.txt'
foreach ($line in [System.IO.File]::ReadLines($path)) {
# traitement ligne
}
Ou en blocs avec PowerShell :
Get-Content .\large.txt -ReadCount 1000 | ForEach-Object {
foreach ($line in $_) { # traitement ligne }
}
Surveillez WS/PM :
Get-Process -Id $PID | Select-Object Id, WS, PM
Éviter Select-Object * et les propriétés inutiles
# Moins efficace
Get-Process | Select-Object * | Where-Object WS -gt 200MB
# Mieux
Get-Process | Select-Object Id, ProcessName, WS | Where-Object WS -gt 200MB
Réutiliser des objets pour l'accumulation
# Exemple construit pour de gros appends
$list = [System.Collections.Generic.List[psobject]]::new()
foreach ($item in 1..200000) {
$null = $list.Add([pscustomobject]@{ N = $item })
}
Contrôles de latence : mesurer l'incontrôlable
La latence vient souvent de l'extérieur. Mesurez-la et adaptez votre stratégie.
Réseau et DNS
Test-NetConnection -ComputerName api.example.test -Port 443
Measure-Command { Resolve-DnsName api.example.test | Out-Null }
REST et commandes distantes
Measure-Command {
Invoke-RestMethod -Uri 'https://api.example.test/status' -Method Get | Out-Null
}
# Fan-out avec limitation (PowerShell 7+)
$uris = 1..20 | ForEach-Object { "https://api.example.test/item/$_" }
Measure-Command {
$results = $uris | ForEach-Object -Parallel {
Invoke-RestMethod -Uri $_ -TimeoutSec 15
} -ThrottleLimit 5
}
Attendu : gains avec un parallélisme modéré, puis plateau ou régression si l'endpoint est saturé.
Réduire les surcoûts évitables
- Désactiver temporairement les progress bars verbeuses, puis restaurer :
$old = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
# traitements massifs
}
finally {
$ProgressPreference = $old
}
- Supprimer la sortie inutile :
$null = Some-Command -Param X
- Minimiser Write-Verbose/Write-Debug dans les boucles serrées.
- Préférer -ErrorAction Stop et try/catch pour échouer vite.
Mettre en musique : workflow minimal
- Baseline
$base = Measure-Command { & .\pilot.ps1 -InputPath .\data.csv -OutPath .\out.csv }
"Baseline: {0: n2}s" -f $base.TotalSeconds
- Segmenter le script
$sw = [System.Diagnostics.Stopwatch]::StartNew()
# Étape A : lecture
$rows = Import-Csv .\data.csv
"Étape A: {0: n2}s" -f $sw.Elapsed.TotalSeconds
# Étape B : transformation
$sw.Restart()
$rows = $rows | Where-Object { $_.Enabled -eq 'true' }
"Étape B: {0: n2}s" -f $sw.Elapsed.TotalSeconds
# Étape C : écriture
$sw.Restart()
$rows | Export-Csv .\out.csv -NoTypeInformation
"Étape C: {0: n2}s" -f $sw.Elapsed.TotalSeconds
- Correctifs ciblés
- Remplacer les boucles chaudes par foreach.
- Paramétrer finement Import-Csv (-Delimiter, -Header).
- Filtrer tôt avant les calculs coûteux.
- Regrouper les écritures (un Export-Csv plutôt que des Add-Content successifs).
- Re-mesurer et comparer à la baseline. Ne garder que les gains nets.
Vérification et diagnostics
Exactitude
$expCount = (Get-Content .\expected.csv).Count
$actCount = (Get-Content .\out.csv).Count
if ($expCount -ne $actCount) { throw 'Row count changed unexpectedly.' }
Import-Csv .\out.csv | Select-Object -First 5
Temps et CPU
Measure-Command { & .\pilot.ps1 -InputPath .\data.csv -OutPath .\out.csv }
Get-Counter '\\Processor(_Total)\\% Processor Time' -SampleInterval 1 -MaxSamples 3
Mémoire et handles
Get-Process -Id $PID | Select-Object ProcessName, Id, WS, PM, Handles
IO
$test = 'C:\\temp\\iobench.bin'
$bytes = New-Object byte[](100MB)
[System.IO.File]::WriteAllBytes($test, $bytes)
Measure-Command { [void][System.IO.File]::ReadAllBytes($test) }
Remove-Item $test -Force
Modes de panne et reprise
- Fan-out qui surcharge une dépendance
- Symptôme : erreurs accrues, timeouts, throttling
- Action : réduire ThrottleLimit, ajouter backoff/retry, repasser en sériel
- Croissance mémoire non bornée
- Symptôme : working set en hausse continue
- Action : streaming (ReadLines, -ReadCount), résultats intermédiaires, traitement par fenêtres
- Progression masquée
- Symptôme : exécution silencieuse trop longue
- Action : journaliser des jalons, toujours restaurer $ProgressPreference
- Écritures partielles
- Symptôme : fichiers tronqués après interruption
- Action : écrire dans un fichier temporaire puis renommer atomiquement
$target = '.\\out.csv'; $temp = "$target.tmp"
Export-Csv $temp -NoTypeInformation -InputObject $rows
Move-Item -Force $temp $target
- Logique modifiée
- Symptôme : comptes ou champs clés différents
- Action : comparer sorties et hachages avant adoption
Modèle de rollback
- Conserver script.ps1.bak.
- En cas de régression, restaurer et re-baseliner :
Copy-Item .\script.ps1.bak .\script.ps1 -Force
& .\script.ps1 -InputPath .\data.csv -OutPath .\out.csv
Techniques pratiques en bref
| Technique | Quand | Exemple |
|---|---|---|
| foreach (mot-clé) | Grandes boucles mémoire | foreach ($x in $data) { ... } |
| Filtres provider | Système de fichiers | Get-ChildItem C:\\Logs -Filter *.log -Recurse |
| IO en flux | Gros fichiers | [System.IO.File]::ReadLines($path) |
| Limiter propriétés | Objets lourds | Select-Object Id, Name |
| Masquer progression | Traitements massifs | $ProgressPreference = 'SilentlyContinue' |
| Parallélisme modéré | Appels distants (PS 7+) | ForEach-Object -Parallel -ThrottleLimit 5 |
Dimensionnement des ressources
Le dimensionnement fixe le plafond. Vérifiez PowerShell 64-bit pour les workloads mémoire :
[Environment]::Is64BitProcess
$PSVersionTable.PSEdition
Capturez CPU/mémoire avant/après un premier pilote :
Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory
Get-Counter '\\Processor(_Total)\\% Processor Time' -SampleInterval 1 -MaxSamples 3
Si la mémoire libre est faible ou le CPU déjà haut, privilégiez streaming et batching plutôt que d'augmenter le parallélisme.
Checklist d'exploitation
- Inventaire
- Version PowerShell, 64-bit, CPU, mémoire libre
- Tailles de données, dépendances distantes, concurrence
- Baseline
- Measure-Command sur dataset représentatif
- Snapshots CPU, mémoire, IO
- Hotspots
- Temporisations légères entre étapes
- Compter itérations et tailles
- Changements sûrs
- foreach pour boucles chaudes
- Filtres provider précoces
- Streaming des gros fichiers ; éviter Select-Object *
- Masquer puis restaurer la progression
- Parallélisme modéré pour appels distants
- Vérifier
- Égalité de sortie ou invariants
- Re-mesure temps/ressources, runs répétés
- Décider et documenter
- Conserver seulement les gains nets
- Noter environnement et mesures
- Plan de rollback
- Garder la version précédente accessible
- Restaurer et re-baseliner en cas de régression
Conclusion
L'optimisation des performances PowerShell est efficace quand elle est pratique, incrémentale et vérifiable. Démarrez petit, mesurez proprement, changez une chose à la fois et facilitez le rollback. Réduisez d'abord le coût par objet et filtrez tôt pour améliorer le débit, puis gérez la latence externe par la mesure et un parallélisme maîtrisé. Validez chaque évolution avec des minutages, des snapshots de ressources et des comparaisons de sortie. Vous obtiendrez des scripts plus rapides, une mémoire plus stable et une fiabilité accrue à mesure que vos charges grandissent.