Considérations relatives aux performances des scripts PowerShell
Les scripts PowerShell qui exploitent directement .NET en évitant le pipeline ont tendance à être plus rapides que le PowerShell idiomatique. Le PowerShell idiomatique utilise des applets de commande et des fonctions PowerShell, en tirant souvent parti du pipeline et en ayant recours à .NET uniquement quand cela est nécessaire.
Remarque
La plupart des techniques décrites ici ne sont pas issues du PowerShell idiomatiques, ce qui peut réduire la lisibilité d’un script PowerShell. Nous conseillons aux auteurs de scripts d’utiliser le PowerShell idiomatique, à moins que les performances ne le permettant pas.
Empêcher la génération de sortie
Il existe de nombreuses façons d’éviter d’écrire des objets dans le pipeline.
- Affectation ou redirection de fichiers vers
$null
- Conversion en
[void]
- Redirection vers
Out-Null
Les vitesses d’affectation à $null
, de conversion en [void]
et de redirection de fichiers vers $null
sont presque identiques. Toutefois, le fait d’appeler Out-Null
dans une grande boucle peut être beaucoup plus lent, notamment dans PowerShell 5.1.
$tests = @{
'Assign to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$null = $arraylist.Add($i)
}
}
'Cast to [void]' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
[void] $arraylist.Add($i)
}
}
'Redirect to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) > $null
}
}
'Pipe to Out-Null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) | Out-Null
}
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ces tests ont été exécutés sur un ordinateur Windows 11 dans PowerShell 7.3.4. Les résultats sont affichés ci-dessous :
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Assign to $null 36.74 1x
10240 Redirect to $null 55.84 1.52x
10240 Cast to [void] 62.96 1.71x
10240 Pipe to Out-Null 81.65 2.22x
51200 Assign to $null 193.92 1x
51200 Cast to [void] 200.77 1.04x
51200 Redirect to $null 219.69 1.13x
51200 Pipe to Out-Null 329.62 1.7x
102400 Redirect to $null 386.08 1x
102400 Assign to $null 392.13 1.02x
102400 Cast to [void] 405.24 1.05x
102400 Pipe to Out-Null 572.94 1.48x
Les temps et les vitesses relatives peuvent varier en fonction du matériel, de la version de PowerShell et de la charge de travail actuelle sur le système.
Ajout de tableaux
La génération d’une liste d’éléments se fait souvent à l’aide d’un tableau avec l’opérateur d’addition :
$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results
L’ajout de tableaux est inefficace, car les tableaux ont une taille fixe. Chaque ajout au tableau crée un autre tableau suffisamment grand pour contenir tous les éléments des opérandes de gauche et de droite. Les éléments des deux opérandes sont copiés dans le nouveau tableau. Pour les petits regroupements, cette surcharge peut être faible. Les performances peuvent être impactées pour les collections volumineuses.
Il existe deux alternatives à cela. Si vous n’avez pas réellement besoin d’un tableau, envisagez plutôt d’utiliser une liste générique typée ([List<T>]
) :
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results
L’impact sur les performances de l’utilisation de l’ajout de tableaux augmente de façon exponentielle selon la taille de la collection et le nombre d’ajouts. Ce code compare explicitement l’affectation de valeurs à un tableau à l’aide de l’ajout de tableau et de l’utilisation de la méthode Add(T)
sur un objet [List<T>]
. Il définit l’affectation explicite comme ligne de base pour les performances.
$tests = @{
'PowerShell Explicit Assignment' = {
param($count)
$result = foreach($i in 1..$count) {
$i
}
}
'.Add(T) to List<T>' = {
param($count)
$result = [Collections.Generic.List[int]]::new()
foreach($i in 1..$count) {
$result.Add($i)
}
}
'+= Operator to Array' = {
param($count)
$result = @()
foreach($i in 1..$count) {
$result += $i
}
}
}
5kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value -Count $_ }).TotalMilliseconds
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ces tests ont été exécutés sur un ordinateur Windows 11 dans PowerShell 7.3.4.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 PowerShell Explicit Assignment 26.65 1x
5120 .Add(T) to List<T> 110.98 4.16x
5120 += Operator to Array 402.91 15.12x
10240 PowerShell Explicit Assignment 0.49 1x
10240 .Add(T) to List<T> 137.67 280.96x
10240 += Operator to Array 1678.13 3424.76x
102400 PowerShell Explicit Assignment 11.18 1x
102400 .Add(T) to List<T> 1384.03 123.8x
102400 += Operator to Array 201991.06 18067.18x
Lorsque vous travaillez avec de grandes collections, l’ajout de tableaux est considérablement plus lent que l’ajout à une List<T>
.
Lorsque vous utilisez un objet [List<T>]
, vous devez créer la liste avec un type spécifique, comme [String]
ou [Int]
. Lorsque vous ajoutez des objets d’un type différent à la liste, ils sont convertis vers le type spécifié. S’ils ne peuvent pas être castés dans le type spécifié, la méthode lève une exception.
$intList = [System.Collections.Generic.List[int]]::new()
$intList.Add(1)
$intList.Add('2')
$intList.Add(3.0)
$intList.Add('Four')
$intList
MethodException:
Line |
5 | $intList.Add('Four')
| ~~~~~~~~~~~~~~~~~~~~
| Cannot convert argument "item", with value: "Four", for "Add" to type
"System.Int32": "Cannot convert value "Four" to type "System.Int32".
Error: "The input string 'Four' was not in a correct format.""
1
2
3
Lorsque vous avez besoin que la liste soit une collection de différents types d’objets, créez-la avec [Object]
comme type de liste. Vous pouvez énumérer la collection et inspecter les types des objets qu’elle contient.
$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList | ForEach-Object { "$_ is $($_.GetType())" }
1 is int
2 is string
3 is double
Si vous avez besoin d’un tableau, vous pouvez appeler la méthode ToArray()
sur la liste ou laisser PowerShell créer le tableau à votre place :
$results = @(
Get-Something
Get-SomethingElse
)
Dans cet exemple, PowerShell crée un [ArrayList]
pour stocker les résultats écrits dans le pipeline à l’intérieur de l’expression de tableau. Juste avant l’affectation à $results
, PowerShell convertit [ArrayList]
en [Object[]]
.
Ajout de chaîne
Les chaînes sont immuables. Chaque ajout à la chaîne crée en fait une nouvelle chaîne suffisamment grande pour contenir tout le contenu des opérandes de gauche et de droite, puis copie les éléments des deux opérandes dans la nouvelle chaîne. Pour les chaînes de petite taille, cette surcharge peut ne pas avoir d’importance. Pour les chaînes volumineuses, cela peut affecter les performances et la consommation de mémoire.
Il existe au moins deux alternatives :
- L’opérateur
-join
concatène les chaînes - La classe .NET
[StringBuilder]
fournit une chaîne mutable
L’exemple suivant compare les performances de ces trois méthodes de création d’une chaîne.
$tests = @{
'StringBuilder' = {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 0..$args[0]) {
$sb = $sb.AppendLine("Iteration $i")
}
$sb.ToString()
}
'Join operator' = {
$string = @(
foreach ($i in 0..$args[0]) {
"Iteration $i"
}
) -join "`n"
$string
}
'Addition Assignment +=' = {
$string = ''
foreach ($i in 0..$args[0]) {
$string += "Iteration $i`n"
}
$string
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ces tests ont été exécutés sur un ordinateur Windows 11 dans PowerShell 7.4.2. La sortie indique que l’opérateur -join
est le plus rapide, suivi de la classe [StringBuilder]
.
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Join operator 14.75 1x
10240 StringBuilder 62.44 4.23x
10240 Addition Assignment += 619.64 42.01x
51200 Join operator 43.15 1x
51200 StringBuilder 304.32 7.05x
51200 Addition Assignment += 14225.13 329.67x
102400 Join operator 85.62 1x
102400 StringBuilder 499.12 5.83x
102400 Addition Assignment += 67640.79 790.01x
Les temps et les vitesses relatives peuvent varier en fonction du matériel, de la version de PowerShell et de la charge de travail actuelle sur le système.
Traitement des fichiers volumineux
La méthode idiomatique pour traiter un fichier dans PowerShell peut ressembler à ceci :
Get-Content $path | Where-Object Length -GT 10
Cela peut s’avérer dix fois plus lent que l’utilisation directe des API .NET. Par exemple, vous pouvez utiliser la classe .NET [StreamReader]
:
try {
$reader = [System.IO.StreamReader]::new($path)
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line.Length -gt 10) {
$line
}
}
}
finally {
if ($reader) {
$reader.Dispose()
}
}
Vous pouvez également utiliser la méthode ReadLines
de [System.IO.File]
, qui encapsule StreamReader
, simplifie le processus de lecture :
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
Recherche d’entrées par propriété dans des collections volumineuses
Il est courant d’avoir à utiliser une propriété partagée pour identifier le même enregistrement dans différentes collections, comme l’utilisation d’un nom pour récupérer un ID d’une liste et un e-mail d’une autre liste. Faire une itération sur la première liste pour rechercher l’enregistrement correspondant dans la deuxième collection est une opération lente. En particulier, le filtrage répété de la deuxième collection a une surcharge importante.
Examinons deux collections : l’une d’elles a les propriétés ID et Nom, et l’autre a les propriétés Nom et E-mail :
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
La méthode habituelle pour rapprocher ces collections afin de retourner une liste d’objets avec les propriétés ID, Nom et E-mail peut ressembler à ceci :
$Results = $Employees | ForEach-Object -Process {
$Employee = $_
$Account = $Accounts | Where-Object -FilterScript {
$_.Name -eq $Employee.Name
}
[pscustomobject]@{
Id = $Employee.Id
Name = $Employee.Name
Email = $Account.Email
}
}
Toutefois, cette implémentation doit filtrer les 5 000 éléments de la collection $Accounts
, à raison d’une fois pour chaque élément de la collection $Employee
. Cette opération peut prendre plusieurs minutes, même pour une recherche d’une seule valeur comme celle-ci.
Plutôt que d’utiliser cette méthode, vous pouvez créer une Table de hachage qui utilise la propriété Nom partagée comme clé et le compte correspondant comme valeur.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
Rechercher des clés dans une table de hachage est beaucoup plus rapide que de filtrer une collection par valeurs de propriété. Au lieu de vérifier chaque élément dans la collection, PowerShell peut simplement vérifier si la clé est définie et utiliser sa valeur.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
C’est beaucoup plus rapide. Alors que le filtre en boucle a pris plusieurs minutes, la recherche par hachage prend moins d’une seconde.
Utiliser Write-Host avec prudence
La commande Write-Host
ne doit être utilisée que lorsque vous devez écrire du texte mis en forme dans la console hôte, plutôt que d’écrire des objets dans le pipeline Réussite.
Write-Host
peut être un ordre de grandeur plus lent que [Console]::WriteLine()
pour des hôtes spécifiques comme pwsh.exe
, powershell.exe
ou powershell_ise.exe
. Toutefois, il n’est pas garanti que [Console]::WriteLine()
fonctionne sur tous les hôtes. De plus, la sortie écrite à l’aide de [Console]::WriteLine()
n’est pas écrite dans les transcriptions démarrées par Start-Transcript
.
JIT (compilation)
PowerShell compile le code de script en bytecode interprété. À partir de PowerShell 3, pour tout code exécuté de façon répétée dans une boucle, PowerShell peut améliorer les performances en effectuant une compilation JIT (juste-à-temps) du code en code natif.
Les boucles qui comportent moins de 300 instructions sont éligibles pour la compilation JIT. Les boucles plus volumineuses sont trop coûteuses à compiler. Lorsque la boucle s’est exécutée 16 fois, le script est compilé juste-à-temps en arrière-plan. Lorsque la compilation JIT est terminée, l’exécution est transférée au code compilé.
Éviter les appels répétés à une fonction
L’appel d’une fonction peut être une opération coûteuse. Si vous appelez une fonction dans une boucle serrée longue, vous pouvez déplacer la boucle à l’intérieur de la fonction.
Penchez-vous sur les exemples suivants :
$tests = @{
'Simple for-loop' = {
param([int] $RepeatCount, [random] $RanGen)
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = $RanGen.Next()
}
}
'Wrapped in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberCore {
param ($rng)
$rng.Next()
}
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = Get-RandomNumberCore -rng $RanGen
}
}
'for-loop in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberAll {
param ($rng, $count)
for ($i = 0; $i -lt $count; $i++) {
$null = $rng.Next()
}
}
Get-RandomNumberAll -rng $RanGen -count $RepeatCount
}
}
5kb, 10kb, 100kb | ForEach-Object {
$rng = [random]::new()
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -RepeatCount $_ -RanGen $rng }
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds,2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
L’exemple Basic for-loop est la ligne de base pour les performances. Le deuxième exemple wrappe le générateur de nombres aléatoires dans une fonction appelée au sein d’une boucle serrée. Le troisième exemple déplace la boucle à l’intérieur de la fonction. La fonction n’est appelée qu’une seule fois, mais le code génère toujours la même quantité de nombres aléatoires. Notez la différence entre les délais d’exécution pour chaque exemple.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 for-loop in a function 9.62 1x
5120 Simple for-loop 10.55 1.1x
5120 Wrapped in a function 62.39 6.49x
10240 Simple for-loop 17.79 1x
10240 for-loop in a function 18.48 1.04x
10240 Wrapped in a function 127.39 7.16x
102400 for-loop in a function 179.19 1x
102400 Simple for-loop 181.58 1.01x
102400 Wrapped in a function 1155.57 6.45x
Éviter de wrapper des pipelines d’applet de commande
La plupart des applets de commande sont implémentées pour le pipeline, qui correspond à une syntaxe et un processus séquentiels. Exemple :
cmdlet1 | cmdlet2 | cmdlet3
L’initialisation d’un nouveau pipeline peut être coûteuse. Vous devez donc éviter de wrapper un pipeline d’applet de commande dans un autre pipeline existant.
Prenons l'exemple suivant. Le fichier Input.csv
contient 2 100 lignes. La commande Export-Csv
est wrappée dans le pipeline ForEach-Object
. L’applet de commande Export-Csv
est appelée pour chaque itération de la boucle ForEach-Object
.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
} | Export-Csv .\Output1.csv -Append
}
}
'Wrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Wrapped = 15,968.78 ms
Pour l’exemple suivant, la commande Export-Csv
a été déplacée en dehors du pipeline ForEach-Object
.
Dans le cas présent, Export-Csv
est appelé une seule fois, mais traite toujours tous les objets passés en dehors de ForEach-Object
.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
}
} | Export-Csv .\Output2.csv
}
'Unwrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Unwrapped = 42.92 ms
L’exemple non wrappé est 372 fois plus rapide. Notez également que la première implémentation nécessite le paramètre Append, qui n’est pas obligatoire pour la prochaine implémentation.
Création d'objet
La création d’objets à l’aide de l’applet de commande New-Object
peut être lente. Le code suivant compare les performances de la création d’objets à l’aide de l’applet de commande New-Object
à l’accélérateur de type [pscustomobject]
.
Measure-Command {
$test = 'PSCustomObject'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = [PSCustomObject]@{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = New-Object -TypeName PSObject -Property @{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
PSCustomObject 0.48
New-Object 3.37
PowerShell 5.0 a ajouté la méthode statique new()
pour tous les types .NET. Le code suivant compare les performances de la création d’objets à l’aide de l’applet de commande New-Object
à la méthode new()
.
Measure-Command {
$test = 'new() method'
for ($i = 0; $i -lt 100000; $i++) {
$sb = [System.Text.StringBuilder]::new(1000)
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList 1000
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
new() method 0.59
New-Object 3.17
Utiliser OrderedDictionary pour créer dynamiquement des objets
Il existe des situations où nous devrons peut-être créer dynamiquement des objets en fonction d’une entrée, la méthode peut-être la plus couramment utilisée pour créer un nouveau PSObject, puis ajouter de nouvelles propriétés à l’aide de l’applet de commande Add-Member
. Le coût des performances pour les petites collections à l’aide de cette technique peut être négligeable, mais il peut devenir très visible pour les grandes collections. Dans ce cas, l’approche recommandée consiste à utiliser un [OrderedDictionary]
, puis à le convertir en PSObject à l’aide de l’accélérateur de type [pscustomobject]
. Pour plus d’informations, consultez la section Création de dictionnaires ordonnés de about_Hash_Tables.
Supposons que vous disposez de la réponse d’API suivante stockée dans la variable $json
.
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{ "name": "Type", "type": "string" },
{ "name": "TenantId", "type": "string" },
{ "name": "count_", "type": "long" }
],
"rows": [
[ "Usage", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "Usage", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "BillingFact", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "BillingFact", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "Operation", "63613592-b6f7-4c3d-a390-22ba13102111", "7" ],
[ "Operation", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "5" ]
]
}
]
}
Supposons maintenant que vous souhaitiez exporter ces données vers un fichier CSV. Tout d’abord, vous devez créer de nouveaux objets et ajouter les propriétés et les valeurs à l’aide de l’applet de commande Add-Member
.
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [psobject]::new()
$index = 0
foreach ($column in $columns) {
$obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
}
$obj
}
À l’aide d’un OrderedDictionary
, le code peut être traduit en :
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [ordered]@{}
$index = 0
foreach ($column in $columns) {
$obj[$column.name] = $row[$index++]
}
[pscustomobject] $obj
}
Dans les deux cas, la sortie $result
serait identique :
Type TenantId count_
---- -------- ------
Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
Cette dernière approche devient exponentiellement plus efficace à mesure que le nombre d’objets et de propriétés membres augmente.
Voici une comparaison des performances de trois techniques de création d’objets avec 5 propriétés :
$tests = @{
'[ordered] into [pscustomobject] cast' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [ordered]@{}
foreach ($prop in $props) {
$obj[$prop] = $i
}
[pscustomobject] $obj
}
}
'Add-Member' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
}
$obj
}
}
'PSObject.Properties.Add' = {
param([int] $iterations, [string[]] $props)
# this is how, behind the scenes, `Add-Member` attaches
# new properties to our PSObject.
# Worth having it here for performance comparison
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj.PSObject.Properties.Add(
[psnoteproperty]::new($prop, $i))
}
$obj
}
}
}
$properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
1kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Et voici les résultats :
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
1024 [ordered] into [pscustomobject] cast 22.00 1x
1024 PSObject.Properties.Add 153.17 6.96x
1024 Add-Member 261.96 11.91x
10240 [ordered] into [pscustomobject] cast 65.24 1x
10240 PSObject.Properties.Add 1293.07 19.82x
10240 Add-Member 2203.03 33.77x
102400 [ordered] into [pscustomobject] cast 639.83 1x
102400 PSObject.Properties.Add 13914.67 21.75x
102400 Add-Member 23496.08 36.72x