Metsys Blog

Powershell – Comment optimiser des scripts complexes ? – Partie 2

Cliquez pour évaluer cet article !
0 avis

Dans cette deuxième partie, nous allons aborder 4 nouveaux points d’optimisation  :

  1. Performances de l’opérateur d’assignation += sur les tableaux
  2. Les retours de valeurs : Out-null , void ou $null
  3. Création de nouveaux objets PowerShell
  4. Performances en écriture dans un fichier de sortie

Performances de l’opérateur d’assignation += sur les tableaux

Les tableaux en PowerShell sont des éléments simples à maîtriser et relativement flexibles a écrire comme à relire. Si le nombre d’éléments à intégrer dans un tableau est élevé, les performances de l’ajout de nouveaux éléments vont être catastrophiques.

Pourquoi ? Tout simplement car l’opérateur += recopie tout le tableau dans une nouvelle variable et ajoute simplement en dernière position la nouvelle valeur.
Donc, plus le nombre d’éléments est élevé, plus le processeur doit recopier d’éléments pour l’ajout d’une valeur.

Nous verrons qu’en dessus de 100 éléments dans le tableau les performances se dégradent rapidement.

Il n’y a actuellement pas de solution pour optimiser ce comportement.

Solution alternative :

La meilleure solution pour remplacer les tableaux standards, est d’appeler directement la classe .net originale « ArrayList ». Celle-ci est native au .net et s’étend dynamiquement quand on ajoute une nouvelle valeur.

A l’ajout d’une nouvelle valeur, l’ArrayList est simplement étendu d’un élément. Il n’y a pas de recopie d’élément. La consommation de la pile d’exécution est ainsi réduite au minimum.
Nous vous invitons à consulter ce PDF très bien documenter (page 17 pour la structure ArrayList) :

L’exemple ci-dessous est valable pour 10 000 objets :

# Performances  Array standard:
# 2789 ms
(measure-command -ex {
$guid = @()
1..10000 | % {
$guid += [guid]::NewGuid().guid
}
}).TotalMilliseconds
# Performances .net Array list:
# 204 ms
(measure-command -ex {
$guid = new-object System.Collections.ArrayList
1..10000 | % {
[void] $guid.Add([guid]::NewGuid().guid)
}
}).TotalMilliseconds

Le même exemple si on remplace la valeur par 100 000 :

  • Avec la méthode .net le script met 1 627 millisecondes à se terminer.
  • Avec l’assignation standard, celui-ci mets 526 Seconds 366 Millisecondes .

Comme on peut le voir plus le nombre d’objets est important, plus l’assignation sera longue avec la méthode standard.

Les retours de valeurs : Out-null , void ou $null

Beaucoup de fonctions retournent un objet ou une valeur, dans certains cas nous ne souhaitons pas afficher cette valeur de retour (inutile, trop verbeuse, non affichable …)
Si on est habitué à utiliser que des commandes PowerShell pour éviter les retours de valeurs d’une fonction, on se tournera naturellement vers la cmdlet associée : out-null.

Exemple :

Get-childitem |out-null

Il existe plusieurs autres possibilités pour annuler la sortie d’une commande :
La variable spéciale $null permettant d’annuler tout ce qui passe par elle :
Exemple :

  • $null = get-childitem
  • Get-childitem > $null
  • get-childitem >> $null

Le mot clé de type [void], souvent utilisé en .net ou C style.
Exemple : [Void]$(1..100)

Après différents tests, toutes les techniques donnent un résultat équivalent (suppression de l’information).

Mais la technique la plus efficace est la redirection [VOID]:

## Standard out
Measure-Command {$(1..10000)}
# 6,8861 Ms
Measure-Command {$(1..10000) | Out-Null}
# 26,8737 Ms
# FASTEST! Measure-Command {[Void]$(1..10000)} # 2,4089 Ms Measure-Command {$(1..10000) > $null} # 3,0825 Ms Measure-Command {$null = $(1..10000)} # 4,627 Ms

Donc n’hésitez pas à préférer [void] au lieu de out-null . Out-null est la méthode la plus lente car nécessite la création d’un Pipe entier pour fonctionner.

Création de nouveaux objets PowerShell

Lorsque nous intensifions un objet PowerShell, celui peut être vide ou alors avoir certaines valeurs (propriétés) déjà remplies. Par exemple, pour un humain : un nom, un âge, une ville et un pays de référence.

Ces valeurs sont ensuite exploitables dans différents cas de figures soient en lecture soit en écriture.
Dans les cas où nous travaillons avec des grosses quantités de données, l’initialisation de ces valeurs prend aussi du temps. Particulièrement sur des cas à plus de 10 000 objets les performances peuvent fortement être impactées.

Beaucoup d’articles proposent la méthode standard PowerShell : New-Object avec l’ajout de propriétés en utilisant add-member.
Exemple typique :

$Person = New-Object psobject
$Person | add-member -notepropertyname Name -NotePropertyValue "Joe"
$Person | add-member -notepropertyname Country -NotePropertyValue "France"
$Person | add-member -notepropertyname City -NotePropertyValue "Paris"
$Person | add-member -notepropertyname Age -NotePropertyValue 30
$Person

Une alternative plus rapide est l’utilisation d’un hash table contenant les valeurs ainsi toutes les propriétés sont assignées en une seule fois:

$Person = [PSCustomObject]@{
 Name = 'Joe'
 Country = 'France'
 City = 'Paris'
 Age = 30
 }
$Person

Le résultat est strictement équivalent au niveau de l’objet généré : même propriété , même accès, même taille ….

Afin de mesurer la différence de performance des deux types de générations d’objets, je vous propose de voir sur un contexte de création de 10 000 et 100 000 objets via les deux méthodes :

# Methode New-object 10 000 itérations
Measure-Command {
1..10000 | ForEach-Object {
$Person = New-Object psobject
$Person | add-member -notepropertyname Name -NotePropertyValue "Joe"
$Person | add-member -notepropertyname Country -NotePropertyValue "France"
$Person | add-member -notepropertyname City -NotePropertyValue "Paris"
$Person | add-member -notepropertyname Age -NotePropertyValue 30
}
} | Out-String
# 1 Seconde 319 Ms
# Méthode Hashtables 100 000 iterations
Measure-Command {
1..10000 | ForEach-Object {
$Person = [PSCustomObject]@{
Name = 'Joe'
Country = 'France'
City = 'Paris'
Age = 30
}
}
} | Out-String
# 35 Milliseconds

Les résultats pour la création de 100 000 objets :

  • New-Object : 12 Seconds 186 Milliseconds
  • Hashtable: 267 Milliseconds

Résultat : Privilégier les hashtables !!! Plus de 10 fois plus rapides sur un exemple de 100 000 objets…

Performances en écriture dans un fichier de sortie

Dans ce chapitre, nous abordons l’écriture d’un ensemble de ligne à partir de 2 fichiers sources différents :

  • 1er fichier : 105 001 lignes
  • 2 eme fichier : 3 255 031 lignes

Ces lignes sont des fichiers de fausses databases générées par import / Export de bases AD.

Le fichier est lu en premier : en une fois et toutes les données sont stockées en RAM (variable : $script:InfosFromAdFullRead0 ).


Après nous le réécrivons en entier suivant 3 méthodes :

  • Redirection (Comme en DOS : « > » )
  • Cmdlet Out-file
  • Class .net Stream.write
$scriptPath = split-path -parent 
$MyInvocation.MyCommand.Definition
$namebaseAD = ".\newDatabaseFullOk_V3.txt"$towriteFileName = "FileOut"
write-host "`nmethod get content READ 0`n"
$script:InfosFromAdFullRead0 = get-content -Path $namebaseAD -ReadCount 0 -Encoding UTF8
write-host "`nmethod Write _Redirection`n"
$towriteFileNameRedirection = $towriteFileName + "_Redirection.txt"
Measure-Command {
$script:InfosFromAdFullRead0 > $towriteFileNameRedirection
}
write-host "`nmethod Write _OutFile`n"
$towriteFileNameOutFile = $towriteFileName + "_OutFile.txt"
Measure-Command {
$towriteFileNameOutFile = $towriteFileName + "_OutFile.txt"
$script:InfosFromAdFullRead0 | Out-File $towriteFileNameOutFile -Force
}
write-host "`nmethod Write Stream`n"
$towriteFileNameStream = $scriptPath + "\" + $towriteFileName + "_Stream.txt"
Measure-Command {
# Ouverture du flux sur le fichier de sortie
[System.IO.StreamWriter]$stream = new-object 'System.IO.StreamWriter' -ArgumentList $towriteFileNameStream , $false
# Ecriture dans le flux de sortie
foreach ($line in $script:InfosFromAdFullRead0)
{
$stream.WriteLine($line)
}
# Fermeture du flux
$stream.close()
}
  • Fichier 1 : 105 001 lignes
    • Redirection : « > »: 3 s 493 ms
    • Cmdlet : Out-file : 3 s 440 ms
    • Class .Net : Stream Write: 0 s 223 ms
  • Fichier 2 : 3 255 031 lignes
    • Redirection : « > »:  1 m 50 s 742 ms
    • Cmdlet : Out-file:  1 m 48 s 385 ms
    • Class .Net : Stream Write: 7 s 140 ms

Résultats : n’hésitez pas à utiliser un flux d’écriture avec Stream Write afin d’optimiser vos opérations en écriture. Celle-ci monopolise moins le CPU et gagne en rapidité. Par contre, la classe .Net Stream Write est un peu plus difficile à prendre en main.

Ci dessous, deux scripts avec et sans les améliorations décrites dans ce post. Les scripts chargent un faux fichier de databases, coupent chaque ligne pour récupérer les infos des deux premières colonnes et réécrit dans un fichier seulement ces deux colonnes.

  • Standard
    • 5047 lines: 4 s 123 ms
    • 10094 Lines: 9 s 978 ms
    • 100940 Lines: 10 m 2 s 665 ms
  • Fatest
    • 5047 lines: 0 s 111 ms
    • 10094 Lines: 0 s 213 ms
    • 100940 Lines: 57 s 450 ms
# Parametres de lancement
Param ([switch]$verbose = $false, [switch]$cls)
# Variables techniques
Set-strictmode -version Latest
#Si parametre verbose, on affiche tous write-verbose
if ($verbose)
{
$VerbosePreference = 'Continue'
}
# Si parametre CLS, on nettoye la console
if ($cls)
{
Clear-Host
}
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$scriptPath
$namebaseAD = ".\NewDB_Generate_100940_Lines.txt"
$towriteFileName = "FileOutSummary"
write-host "`nmethod get content READ 0`n"
# array of lines content
# get content file
$script:InfosFromAdFullRead0 = get-content -Path $namebaseAD -ReadCount 0 -Encoding UTF8
write-host "Numbers of lines loaded --> $($script:InfosFromAdFullRead0.count)"
# "Fastest method"
write-host "Try Fastest method"
Measure-Command {
# Init Array list container
$arrayListContentSplit = new-object System.Collections.ArrayList
# Determine File destination name
$towriteFileNameStreamExample = $scriptPath + "\" + $towriteFileName + "_fast.txt"
# For each line, cut (separator ';') and keep only First and second Elements.
foreach ($theline in $script:InfosFromAdFullRead0)
{
# Cut
$arrayLine = $theLine.split(';')
# Create new object and assign values
$objToAddHT = [PSCustomObject]@{
FirstPositionArray = $arrayLine[0]
SecondPositionArray = $arrayLine[1]
}
# Add to arraylist container
# Evict value return (Void)
[void] $arrayListContentSplit.Add($objToAddHT)
}
# Get handle stream on destination file
[System.IO.StreamWriter]$stream = new-object 'System.IO.StreamWriter' -ArgumentList $towriteFileNameStreamExample , $false
# write (on the stream!) each line
foreach ($line in $arrayListContentSplit)
{
$stream.WriteLine( $line.FirstPositionArray + "`t" + $line.SecondPositionArray)
}
# Close & write on disk
$stream.close()
} # End mesure "Fastest methods"
write-host "Try Standard method"
# "Standards methods"
Measure-Command {
# init array
$arrayContentSplit = @()
# Determine File destination name
$towriteFileNameOutFileExample = $towriteFileName + "_slow.txt"
# For each line, cut (separator ';') and keep only First and second Elements.
foreach ($theLine in $script:InfosFromAdFullRead0)
{
# Cut
$arrayLine = $theLine.split(';')
# Create new object and assign values
$objToAdd = New-Object psobject
$objToAdd | add-member -notepropertyname FirstPositionArray -NotePropertyValue $arrayLine[0]
$objToAdd | add-member -notepropertyname SecondPositionArray -NotePropertyValue $arrayLine[1]
# add to array
$arrayContentSplit += $objToAdd
} #End foreach
# Write content on file
$arrayContentSplit | Out-File $towriteFileNameOutFileExample -Force
} # End mesure "Standards methods"
# Exit script standard method
# 0 = no problem
exit 0

Notez cet article

Vous avez aimé cet article ?

Rendez-le plus visible auprès des internautes en lui mettant une bonne note.

Cliquez pour évaluer cet article !
0 avis

Articles pouvant vous intéresser

RETEX CERT

Tout d’abord, en termes d’éthique et pour respecter la confidentialité des sujets aussi sensibles que