A PowerShell-szkriptek teljesítményével kapcsolatos szempontok

A .NET-et közvetlenül használó és a folyamatot elkerülő PowerShell-szkriptek általában gyorsabbak, mint az idiomatikus PowerShell. Az idiomatikus PowerShell parancsmagokat és PowerShell-függvényeket használ, gyakran használja a folyamatot, és csak akkor használja a .NET-et, ha szükséges.

Feljegyzés

Az itt ismertetett technikák közül számos nem idiomatikus PowerShell, és csökkentheti a PowerShell-szkriptek olvashatóságát. A szkriptkészítőknek ajánlott idiomatikus PowerShellt használniuk, hacsak a teljesítmény másként nem diktál.

Kimenet letiltása

Számos módon kerülheti el, hogy objektumokat írjon a folyamatba.

  • Hozzárendelés a $null
  • Öntés a [void]
  • Fájl átirányítása ide: $null
  • Cső a Out-Null

A hozzárendelés $null, a kiosztás [void]és a fájlátirányítás $null sebessége szinte azonos. A nagy hurok hívása Out-Null azonban jelentősen lassabb lehet, különösen a PowerShell 5.1-ben.

$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'
        }
    }
}

Ezeket a teszteket Windows 11 rendszerű gépen futtatták a PowerShell 7.3.4-ben. Az eredmények az alábbiakban láthatók:

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

Az idő és a relatív sebesség a hardvertől, a PowerShell verziójától és a rendszer aktuális számítási feladataitól függően változhat.

Tömb hozzáadása

Az elemek listájának létrehozása gyakran egy tömb használatával történik, a hozzáadás operátorral:

$results = @()
$results += Do-Something
$results += Do-SomethingElse
$results

A tömbök hozzáadása nem hatékony, mert a tömbök mérete rögzített. A tömb minden egyes hozzáadása létrehoz egy új tömböt, amely elég nagy ahhoz, hogy mind a bal, mind a jobb operandus összes elemét megtartsa. A rendszer mindkét operandus elemeit az új tömbbe másolja. A kis gyűjtemények esetében ez a többletterhelés nem feltétlenül számít. A nagy gyűjtemények teljesítménycsökkenést okozhatnak.

Van néhány alternatíva. Ha valójában nincs szüksége tömbre, fontolja meg egy beírt általános lista (T> lista<) használatát:

$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Do-Something))
$results.AddRange((Do-SomethingElse))
$results

A tömbök összeadásának teljesítményhatása exponenciálisan nő a gyűjtemény méretével és a szám összeadásaival. Ez a kód összehasonlítja az értékek tömbhöz való explicit hozzárendelését a tömbök összeadásával és a T> lista<metódusának Add()használatával. Explicit hozzárendelést határoz meg a teljesítmény alapkonfigurációjaként.

$tests = @{
    'PowerShell Explicit Assignment' = {
        param($count)

        $result = foreach($i in 1..$count) {
            $i
        }
    }
    '.Add(..) 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'
        }
    }
}

Ezeket a teszteket Windows 11 rendszerű gépen futtatták a PowerShell 7.3.4-ben.

CollectionSize Test                           TotalMilliseconds RelativeSpeed
-------------- ----                           ----------------- -------------
          5120 PowerShell Explicit Assignment             26.65 1x
          5120 .Add(..) to List<T>                       110.98 4.16x
          5120 += Operator to Array                      402.91 15.12x
         10240 PowerShell Explicit Assignment              0.49 1x
         10240 .Add(..) to List<T>                       137.67 280.96x
         10240 += Operator to Array                     1678.13 3424.76x
        102400 PowerShell Explicit Assignment             11.18 1x
        102400 .Add(..) to List<T>                      1384.03 123.8x
        102400 += Operator to Array                   201991.06 18067.18x

Ha nagy gyűjteményekkel dolgozik, a tömbök hozzáadása jelentősen lassabb, mint a T> lista<hozzáadása.

A T> lista<használatakor létre kell hoznia a listát egy adott típussal, például sztringgel vagy inttel. Ha más típusú objektumokat ad hozzá a listához, azok a megadott típusba kerülnek. Ha nem lehet a megadott típusra leadni őket, a metódus kivételt hoz létre.

$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

Ha azt szeretné, hogy a lista különböző típusú objektumok gyűjteménye legyen, hozza létre az objektumot listatípusként. A gyűjtemény számbavételével megvizsgálhatja a benne lévő objektumok típusait.

$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList.GetEnumerator().ForEach({ "$_ is $($_.GetType())" })
1 is int
2 is string
3 is double

Ha tömbre van szüksége, meghívhatja a ToArray() metódust a listában, vagy engedélyezheti a PowerShell számára a tömb létrehozását:

$results = @(
    Do-Something
    Do-SomethingElse
)

Ebben a példában a PowerShell létrehoz egy Tömblistát , amely a tömbkifejezésen belül a folyamatba írt eredményeket tárolja. A PowerShell a hozzárendelés $resultselőtt objektummá alakítja a Tömblistát[].

Sztring hozzáadása

A sztringek nem módosíthatók. A sztring minden egyes hozzáadása létrehoz egy új sztringet, amely elég nagy ahhoz, hogy mind a bal, mind a jobb operandus tartalmát megtartsa, majd mindkét operandus elemeit átmásolja az új sztringbe. Kis sztringek esetén ez a többletterhelés nem feltétlenül számít. Nagy sztringek esetén ez hatással lehet a teljesítményre és a memóriahasználatra.

Legalább két alternatíva létezik:

  • Az -join operátor összefűzi a sztringeket
  • A .NET StringBuilder osztály egy mutable sztringet biztosít

Az alábbi példa a sztringek létrehozásának három módszerét hasonlítja össze.

$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'
        }
    }
}

Ezeket a teszteket Windows 10 rendszerű gépen futtatták a PowerShell 7.3.4-ben. A kimenet azt mutatja, hogy az -join operátor a leggyorsabb, amelyet a StringBuilder osztály követ.

Iterations Test                   TotalMilliseconds RelativeSpeed
---------- ----                   ----------------- -------------
     10240 Join operator                       7.08 1x
     10240 StringBuilder                      54.10 7.64x
     10240 Addition Assignment +=            724.16 102.28x
     51200 Join operator                      41.76 1x
     51200 StringBuilder                     318.06 7.62x
     51200 Addition Assignment +=          17693.06 423.68x
    102400 Join operator                     106.98 1x
    102400 StringBuilder                     543.84 5.08x
    102400 Addition Assignment +=          90693.13 847.76x

Az idő és a relatív sebesség a hardvertől, a PowerShell verziójától és a rendszer aktuális számítási feladataitól függően változhat.

Nagyméretű fájlok feldolgozása

A Fájlok PowerShellben történő feldolgozásának idiomatikus módja a következőhöz hasonló lehet:

Get-Content $path | Where-Object { $_.Length -gt 10 }

Ez nagyságrenddel lassabb lehet, mint a .NET API-k közvetlen használata:

try
{
    $stream = [System.IO.StreamReader]::new($path)
    while ($line = $stream.ReadLine())
    {
        if ($line.Length -gt 10)
        {
            $line
        }
    }
}
finally
{
    $stream.Dispose()
}

Bejegyzések keresése tulajdonság szerint nagy gyűjteményekben

Gyakran előfordul, hogy egy megosztott tulajdonság használatával azonos rekordot kell azonosítani a különböző gyűjteményekben, például egy névvel lekérni egy azonosítót az egyik listából, és egy e-mailt egy másikból. Lassú az iterálás az első listában, hogy megtalálja az egyező rekordot a második gyűjteményben. Különösen a második gyűjtemény ismételt szűrése nagy többletterheléssel jár.

Két gyűjteményt adott meg, az egyik azonosítóval és névvel, a másik pedig névvel és e-mail-címmel:

$Employees = 1..10000 | ForEach-Object {
    [PSCustomObject]@{
        Id   = $_
        Name = "Name$_"
    }
}

$Accounts = 2500..7500 | ForEach-Object {
    [PSCustomObject]@{
        Name = "Name$_"
        Email = "Name$_@fabrikam.com"
    }
}

A gyűjtemények egyeztetésének szokásos módja, hogy visszaadja az objektumok listáját az azonosító, a név és az e-mail tulajdonságaival, a következőképpen nézhet ki:

$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
    }
}

Ennek a megvalósításnak azonban a gyűjteményben lévő $Accounts 5000 elemet egyszer kell szűrnie a gyűjtemény minden elemére $Employee . Ez akár perceket is igénybe vehet, még ehhez az egyértékű kereséshez is.

Ehelyett létrehozhat egy kivonattáblát , amely kulcsként a megosztott név tulajdonságot, az egyező fiókot pedig értékként használja.

$LookupHash = @{}
foreach ($Account in $Accounts) {
    $LookupHash[$Account.Name] = $Account
}

A kulcsok keresése egy kivonattáblában sokkal gyorsabb, mint a gyűjtemények tulajdonságértékek szerinti szűrése. A gyűjtemény minden elemének ellenőrzése helyett a PowerShell ellenőrizheti, hogy a kulcs definiálva van-e, és használja-e az értékét.

$Results = $Employees | ForEach-Object -Process {
    $Email = $LookupHash[$_.Name].Email
    [pscustomobject]@{
        Id    = $_.Id
        Name  = $_.Name
        Email = $Email
    }
}

Ez sokkal gyorsabb. Amíg a ciklusszűrő végrehajtása percekig tartott, a kivonatkeresés kevesebb mint egy másodpercet vesz igénybe.

Írási gazdagép elkerülése

Általában rossz gyakorlatnak számít a kimenet közvetlenül a konzolra írása, de ha van értelme, sok szkript használja Write-Host.

Ha sok üzenetet kell írnia a konzolra, nagyságrendekkel lassabb lehet, Write-Host mint [Console]::WriteLine() bizonyos gazdagépek esetében, például pwsh.exe, powershell.exevagy powershell_ise.exe. Azonban nem garantált, [Console]::WriteLine() hogy minden gazdagépen működik. Emellett a használatával [Console]::WriteLine() írt kimenet nem a kezdőbetűs Start-Transcriptátiratokra lesz megírva.

A használat Write-Hosthelyett fontolja meg a Write-Output használatát.

JIT-összeállítás

A PowerShell lefordítja a szkriptkódot az értelmezett bájtkódra. A PowerShell 3-tól kezdődően a ciklusban ismétlődően végrehajtott kód esetében a PowerShell képes javítani a teljesítményt úgy, hogy a kódot natív kódba rendezi.

A 300-nál kevesebb utasítást tartalmazó hurkok jogosultak a JIT-fordításra. Azoknál nagyobb hurkok túl költségesek a fordításhoz. Amikor a ciklus 16 alkalommal lett végrehajtva, a szkript jiT-fordítást végez a háttérben. Amikor a JIT-fordítás befejeződik, a végrehajtás átkerül a lefordított kódba.

A függvények ismételt hívásának elkerülése

A függvények meghívása költséges művelet lehet. Ha egy függvényt hosszú ideig futó szoros hurokban hív meg, fontolja meg a hurok áthelyezését a függvényen belül.

Tekintse az alábbi példákat:

$ranGen = New-Object System.Random
$RepeatCount = 10000

'Basic for-loop = {0}ms' -f (Measure-Command -Expression {
    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = $ranGen.Next()
    }
}).TotalMilliseconds

'Wrapped in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_Core {
        param ($ranGen)
        $ranGen.Next()
    }

    for ($i = 0; $i -lt $RepeatCount; $i++) {
        $Null = Get-RandNum_Core $ranGen
    }
}).TotalMilliseconds

'For-loop in a function = {0}ms' -f (Measure-Command -Expression {
    function Get-RandNum_All {
        param ($ranGen)
        for ($i = 0; $i -lt $RepeatCount; $i++) {
            $Null = $ranGen.Next()
        }
    }

    Get-RandNum_All $ranGen
}).TotalMilliseconds

Az alapszintű for-loop példa a teljesítmény alapvonala. A második példa a véletlenszerű számgenerátort egy szoros hurokban hívott függvénybe csomagolja. A harmadik példa a ciklust a függvényen belülre helyezi. A függvény csak egyszer van meghívva, de a kód továbbra is 10000 véletlenszerű számot hoz létre. Figyelje meg az egyes példák végrehajtási idejének különbségét.

Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms

Kerülje a parancsmag-folyamatok körbefuttatását

A legtöbb parancsmag implementálva van a folyamathoz, amely szekvenciális szintaxis és folyamat. Példa:

cmdlet1 | cmdlet2 | cmdlet3

Az új folyamatok inicializálása költséges lehet, ezért kerülje a parancsmagfolyamatok egy másik meglévő folyamatba való burkolását.

Gondolja át a következő példát. A Input.csv fájl 2100 sort tartalmaz. A Export-Csv parancs be van csomagolva a ForEach-Object folyamatba. A Export-Csv parancsmag a ciklus minden iterációjára ForEach-Object meghívódik.

'Wrapped = {0:N2} ms' -f (Measure-Command -Expression {
    Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
        [PSCustomObject]@{
            Id = $Id
            Name = $_.opened_by
        } | Export-Csv .\Output1.csv -Append
    }
}).TotalMilliseconds

Wrapped = 15,968.78 ms

A következő példában a parancs a Export-Csv folyamaton kívülre ForEach-Object került. Ebben az esetben Export-Csv a rendszer csak egyszer hívja meg, de továbbra is feldolgozza az összes objektumot, amelyből ForEach-Objectki lett adva.

'Unwrapped = {0:N2} ms' -f (Measure-Command -Expression {
      Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
          [PSCustomObject]@{
              Id = $Id
              Name = $_.opened_by
          }
      } | Export-Csv .\Output2.csv
  }).TotalMilliseconds

Unwrapped = 42.92 ms

A le nem írt példa 372-szer gyorsabb. Azt is figyelje meg, hogy az első implementációhoz szükség van a Append paraméterre, amely nem szükséges a későbbi implementációhoz.